# Drizz Docs

Drizz automates mobile apps with Vision AI. Write tests in plain English and run them on Android and iOS.

Drizz automates native Android, iOS and mobile web apps using Vision AI. Tests are written in plain English and run on both platforms without selectors, locators or XPATH.

{% hint style="success" %}
New to Drizz — the [Quickstart](/start-here/quickstart) takes about 10 minutes and ends with a passing test.
{% endhint %}

## Get started

|                                                               |                                                               |
| ------------------------------------------------------------- | ------------------------------------------------------------- |
| [**Introduction**](/start-here/introduction)                  | What Drizz is and how a step executes                         |
| [**Quickstart**](/start-here/quickstart)                      | Install, connect a device, run your first test                |
| [**Platform & device support**](/start-here/platform-support) | What runs where — Android, iOS, local, cloud, private devices |
| [**Use cases**](/start-here/use-cases)                        | Scenarios Drizz automates, and what it does not cover         |

## Set up the desktop app

|                                                             |                                                        |
| ----------------------------------------------------------- | ------------------------------------------------------ |
| [**Download & install**](/desktop-app/download-and-install) | Get the app onto your Mac                              |
| [**Sign in & get access**](/desktop-app/sign-in-and-access) | Workspace accounts, organizations, trials              |
| [**Set up a device**](/desktop-app/device-setup-wizard)     | Guided wizard for Android emulators and iOS simulators |
| [**Tour of the interface**](/desktop-app/interface-tour)    | Editor, device panel, console, Memory                  |

## Write tests

|                                                               |                                                |
| ------------------------------------------------------------- | ---------------------------------------------- |
| [**The shape of a test**](/writing-tests/writing-tests)       | Structure, comments, the command palette       |
| [**Command index**](/writing-tests/command-index)             | Every command Drizz understands, in one table  |
| [**Which variable do I use?**](/writing-tests/which-variable) | Store, SET, datasets and placeholders compared |
| [**Modules**](/writing-tests/modules)                         | Reusable flows, called from any test           |
| [**Memory & blockers**](/writing-tests/memory-and-blockers)   | Dismiss popups automatically across a suite    |
| [**Recipes**](/writing-tests/recipes)                         | Complete, runnable end-to-end examples         |

## Run tests

|                                                                 |                                              |
| --------------------------------------------------------------- | -------------------------------------------- |
| [**Test plans**](/running-tests/test-plans)                     | Group tests, choose devices, set concurrency |
| [**Devices**](/running-tests/devices)                           | Local, cloud and private devices             |
| [**Self-healing**](/running-tests/self-healing)                 | How Drizz repairs failed steps mid-run       |
| [**Reading a report**](/reports-and-debugging/reading-a-report) | Steps, screenshots, statuses, recordings     |

## Automate & integrate

|                                                          |                                                   |
| -------------------------------------------------------- | ------------------------------------------------- |
| [**API overview**](/automate-and-integrate/api-overview) | Authenticate, upload a build, trigger a run       |
| [**CI/CD**](/automate-and-integrate/ci-cd)               | GitHub Actions, Jenkins, GitLab, Bitbucket, Azure |
| [**Jira**](/automate-and-integrate/jira)                 | Raise issues from failed tests                    |

## Reference

|                                                               |                                         |
| ------------------------------------------------------------- | --------------------------------------- |
| [**Troubleshooting**](/reports-and-debugging/troubleshooting) | Symptom, then fix                       |
| [**Known limitations**](/reference/known-limitations)         | Platform gaps and unsupported scenarios |
| [**Glossary**](/reference/glossary)                           | Terms used across these docs            |
| [**Changelog**](/reference/changelog)                         | What changed and when                   |

***

*Last updated: 6 August 2026*


# Introduction

How Drizz works — Vision AI, plain-English commands, and the five stages every test step runs through.

Drizz automates mobile apps from plain-English instructions, resolving each target visually at run time.

|                         |                                                                                                                                             |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **Platforms**           | Android · iOS · Mobile web                                                                                                                  |
| **What you write**      | Plain English, one instruction per line                                                                                                     |
| **What you don't need** | No selectors, no XPath, no accessibility IDs etc                                                                                            |
| **Portability**         | The same file runs on Android and iOS                                                                                                       |
| **Watch out**           | Ambiguous descriptions resolve to different elements between runs — use the app's exact visible wording for better control and stable tests |

## A test looks like this

```
# ShopEase — search for a product

OPEN_APP com.shopease.android
Wait Until 5 Seconds

Validate that the home screen is visible

Tap on the search icon
Type running shoes in the search field
Tap on the first search result
Wait Until 3 Seconds

Validate that the product detail page is visible
```

No setup file, no page objects, no element locators.

## What Drizz does not require

|                                |                                                                                                                                                                                               |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **No selectors**               | <p>A step names the element as it appears: <br><code>Tap on Login CTA</code>, <br>not <code>driver.findElement(By.id("btn\_login\_primary"))</code>. Renaming an ID does not break a test</p> |
| **One script, both platforms** | A step describes intent; Drizz resolves it against the current screen. Some commands differ per platform — see [Known limitations](/reference/known-limitations)                              |
| **No instrumentation**         | No SDK, no debug build, no test code in the app. Drizz drives the app from outside                                                                                                            |

## How a step executes

Every line runs through the same five stages.

| Stage            | What happens                                                                                                                  |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **1 · Capture**  | The system captures the current screen and uses available visual context to better understand what’s displayed.               |
| **2 · Classify** | The line is routed to a command type — tap, type, validate, scroll, system. The first word of the line determines the routing |
| **3 · Locate**   | Vision AI finds the target described and returns its position. This stage fails when a description is ambiguous               |
| **4 · Act**      | The tap, type or swipe happens at that position                                                                               |
| **5 · Verify**   | Drizz confirms the screen changed as the step implies. A `Validate` gets up to **3 attempts** before failing                  |

Note: The 3 attempts do not replace a wait. Add `Wait Until <n> Seconds` before a step that runs after navigation — see [Waits & timing](/writing-tests/waits-and-timing).

## Automatic behavior

|                |                                                                                                                                                                                                                |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Caching**    | Drizz reuses how it resolved a step on a screen it has seen before. Cached steps run faster and cost fewer DT, so a suite speeds up over its first few runs. See [Caching](/running-tests/caching)             |
| **Unblocking** | A popup in the way — a permission prompt, a promo sheet, a network retry — is dismissed using the app's blocker rules, with no step in the script. See [Memory & blockers](/writing-tests/memory-and-blockers) |

## Common mistakes

| What you write                                             | What happens                                                                                                                                                 |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Tap on the button`                                        | Resolves to a different element depending on screen state. Quote the element's visible label                                                                 |
| `Tap on Proceed to Pay` when the button reads `Charge Now` | Paraphrasing misses the element, or hits the wrong one. For strict logic use double quotes, else the test runs purely on the intent to pass the screen state |
| `Scroll` with no direction                                 | Unreliable. Always give a direction and quote the target                                                                                                     |

## Next

* [Quickstart: your first test](/start-here/quickstart)
* [Platform & device support](/start-here/platform-support)
* [Use cases](/start-here/use-cases)

***

*Last updated: 6 August 2026*


# Quickstart: Your first test

Write and run your first Drizz test in about 10 minutes, on a device connected to the desktop app.

Connect a device, write commands in individual lines, and run them locally from the desktop app.

|                   |                                                                                                  |
| ----------------- | ------------------------------------------------------------------------------------------------ |
| **Time**          | \~10 minutes, most of it device boot                                                             |
| **Platforms**     | Android · iOS                                                                                    |
| **Where it runs** | Locally, on the device connected to your Mac; Real or Emulator/Simulator                         |
| **Watch out**     | On an Android emulator, disable the soft keyboard first — it covers elements and makes taps fail |

## Prerequisites

* Drizz desktop app installed and signed in — see [Download & install](/desktop-app/download-and-install) and [Sign in & get access](/desktop-app/sign-in-and-access). Work email addresses only
* One Android emulator, iOS simulator or attached physical device, created with the [guided setup wizard](/desktop-app/device-setup-wizard) — the supported setup path. Opening the Connect Device screen with nothing connected starts the wizard
* Soft keyboard disabled, on an Android emulator

{% hint style="success" %}
Disable the Android emulator's soft keyboard before the first run. It is the most common cause of unexplained tap failures.

```bash
adb shell ime disable com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME
adb shell settings put global show_ime_with_hard_keyboard 0
```

{% endhint %}

## Copy this

A complete test against ShopEase, the demo app used throughout these docs.

{% tabs %}
{% tab title="Android" %}

```
# ShopEase — search for a product and open it

OPEN_APP com.shopease.android
Wait Until 5 Seconds

Validate that the home screen is visible

Tap on the search icon
Wait Until 2 Seconds

Type running shoes in the search field
Tap on the first search result
Wait Until 3 Seconds

Validate that the product detail page is visible
```

{% endtab %}

{% tab title="iOS" %}

```
# ShopEase — search for a product and open it

OPEN_APP com.shopease.ios
Wait Until 5 Seconds

Validate that the home screen is visible

Tap on the search icon
Wait Until 2 Seconds

Type running shoes in the search field
Tap on the first search result
Wait Until 3 Seconds

Validate that the product detail page is visible
```

The bundle ID is the only difference from the Android test.
{% endtab %}
{% endtabs %}

## Steps

1. Open the desktop app and go to the **Connect Device** screen.
2. Select the emulator, simulator or attached device and wait for it to show as connected. Unlock a physical Android device first — Drizz refuses a locked device.
3. If the app under test (AUT) is not already installed, use the **Connect Device** modal to browse and select the APK or IPA file from your local device. The app will be uploaded to the connected device.

   Note: If the AUT is already installed, select it from the app list. No upload is required.
4. Create a test file in the editor.
5. Press `/` on an empty line to open the command palette — the authoritative list of supported commands. The editor prompts *"Type '/' for Drizz supported commands"*.
6. Paste the test above.
7. Replace three values: the package name, `running shoes`, and the two screen descriptions, so they match the app under test.
8. Run the test and watch the console panel.
9. Confirm the run banner reads passed and every line carries a green decorator in the editor.

## What you see during a run

* **Live execution** on the connected, one per step, as each step executes.
* Live logs, shows exact reason of every execution, command by command.&#x20;
* **A status decorator** in the editor beside each line: running, passed, failed.
* **Token usage** after execution. Wait steps are free.

A passing run takes 30–60 seconds on a warm emulator. The first run is the slowest: Drizz caches how it resolved each step on each screen, so the second and third runs of the same test are faster; if the test command and respective screen remains same.

## If a step fails

Download Debug Report: Open the before-screenshot of the failing step.

| What you see                             | Fix                                                                                                               |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| A screen you didn't expect               | The failure is in an earlier step. Fix that one first                                                             |
| The right screen, element visible        | The description doesn't match. Use the app's exact on-screen text                                                 |
| The right screen, element below the fold | Add `Scroll down until "Add to Cart" is visible` before the step                                                  |
| A spinner, screen mid-load               | Add or lengthen the `Wait Until <n> Seconds` before the step                                                      |
| A popup covering everything              | Dismiss it once in Memory rather than in every test — see [Memory & blockers](/writing-tests/memory-and-blockers) |
| A black screen                           | A secure screen. The OS blocks capture there, for Drizz and every other tool                                      |

**Report Issue** lets you report an issue you’re currently experiencing with the platform. Use it to provide details about the problem so the platform team can investigate it.

## Common mistakes

| What you write                           | What happens                                                                                              |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `OPEN_APP` with no package               | Fails by design. The package argument is required                                                         |
| No wait after `OPEN_APP`                 | The next command runs against a splash screen and fails                                                   |
| `Tap on the button`                      | Ambiguous — resolves differently between runs                                                             |
| `Wait until the page loads`              | Not a fixed duration. Write `Wait Until 5 Seconds`                                                        |
| Ten-second waits everywhere as insurance | <p>The suite gets slow and a real timing bug </p><p>stays hidden</p>                                      |
| Wrong scroll direction                   | Scroll direction is determined by the thumb’s movement on the UI, mimicking how a real user would scroll. |

## Next

* [The shape of a test](/writing-tests/writing-tests)
* [Command index](/writing-tests/command-index)
* [When a step fails](/reports-and-debugging/when-a-step-fails)

***

*Last updated: 6 August 2026*


# Platform & Device support

Which platforms and device types Drizz supports, and where each one runs — locally, in the Drizz cloud, or on private devices.

Drizz automates native Android, native iOS and mobile web. Physical iPhones and iPads are supported, not only simulators.

|                 |                                                        |
| --------------- | ------------------------------------------------------ |
| **Platforms**   | Android · iOS · Mobile web                             |
| **Android**     | Emulator · Physical device                             |
| **iOS**         | Simulator · Physical iPhone/iPad                       |
| **Run targets** | Local (your Mac) · Drizz device cloud · Private device |

## Prerequisites

* macOS host, Apple Silicon or Intel
* Drizz desktop app installed and signed in
* Xcode, opened at least once — iOS only
* Desktop app — physical iPhone or iPad only
* An Apple development team, free or paid — physical iPhone or iPad only

## Support matrix

| Platform       | Device type                   | Local | Drizz cloud |
| -------------- | ----------------------------- | ----- | ----------- |
| **Android**    | Emulator                      | ✅     | ✅           |
| **Android**    | Physical device (USB)         | ✅     | ✅           |
| **iOS**        | Simulator                     | ✅     | ✅           |
| **iOS**        | Physical iPhone / iPad (USB)  | ✅     | ✅           |
| **Mobile web** | Browser on a connected device | ✅     | ✅           |

✅ Supported · ❌ Not applicable&#x20;

**Public/Private devices** on cloud are hardware your organization owns, reserved for your org — used for compliance requirements, specific handsets, and SIM, carrier testing etc.

## Desktop app and Drizz Cloud

|                    | **Desktop app**                                           | **Drizz Cloud**                                       |
| ------------------ | --------------------------------------------------------- | ----------------------------------------------------- |
| **Runs on**        | Your Mac, against a connected or emulated physical device | Drizz infrastructure, against the device cloud        |
| **Used for**       | Authoring: Writing tests, debugging, iteration            | Orchestration: Suites, regression, CI, scheduled runs |
| **Parallelism**    | One device at a time                                      | Many tests at once                                    |
| **Feedback**       | Live — the device and console together                    | A report after the run                                |
| **App under test** | Installed on the connected device                         | A registered app, uploaded to Drizz cloud             |

A test file is identical on both surfaces. A test written against a local emulator runs in the cloud unedited when the registered build matches. Editing tests on local changes the same on device.&#x20;

| Task                                | Surface                                             |
| ----------------------------------- | --------------------------------------------------- |
| Writing a new test                  | Desktop app                                         |
| Debugging one failing step          | Desktop app — the device is visible during the run  |
| Running 40 tests before a release   | Cloud                                               |
| Running one test across six devices | Cloud                                               |
| Gating a build in CI                | Cloud, via a [test plan](/running-tests/test-plans) |
| Testing on hardware your org owns   | Cloud, on a private device                          |

## Device setup

{% tabs %}
{% tab title="Android" %}
**Emulator.** The [guided setup wizard](/desktop-app/device-setup-wizard) creates a Pixel 7 profile named **DrizzPhone** on an Android 14 (API 34) Google Play ARM64 image.

**Physical device:**

1. Enable **Developer options** and **USB debugging** on the device.
2. Connect the device by USB.
3. Accept the debugging prompt on the device.
4. Run `adb devices` and confirm it lists as `device`, not `unauthorized`.
5. Unlock the device. Drizz checks lock state and refuses a locked device.
   {% endtab %}

{% tab title="iOS" %}
**Simulator:**

1. Install Xcode and open it once to complete first-run setup.
2. Launch a simulator.
3. Select the simulator in Connect Device. Drizz builds and installs WebDriverAgent on first connect — one to two minutes the first time, cached afterwards.

**Physical iPhone or iPad**, from desktop app v1.8.0 on both Apple Silicon and Intel Macs:

1. Install Xcode on the Mac.
2. Connect the device by USB and trust the Mac when prompted.
3. Enable **Developer Mode** on the device: Settings → Privacy & Security → Developer Mode, then restart the device.
4. Select an **Apple development team** in the desktop app.
5. Trust the developer certificate: Settings → General → VPN & Device Management → your certificate → Trust.

Build requirements for physical hardware:

* A simulator-only build does not install on a physical device.
* An app installed from the App Store cannot be replaced by a sideloaded build of the same bundle ID. Uninstall the store version first.
* iOS test plans in the cloud require a real-device build.

{% hint style="warning" %}
**Free Apple developer accounts expire weekly.** Provisioning profiles from a free team last 7 days, and there's a cap on sideloaded apps. Use a paid Apple Developer account for anything beyond a trial.
{% endhint %}
{% endtab %}
{% endtabs %}

## Mobile web

Automated through a browser on a connected Android emulator or iOS simulator. The same commands apply; Drizz reads the rendered page visually rather than through the DOM.

## Host machine

The desktop app is a signed macOS `.dmg`, in Apple Silicon and Intel builds. See [Download & install](/desktop-app/download-and-install).

## Common mistakes

| What you do                                              | What happens                                                 |
| -------------------------------------------------------- | ------------------------------------------------------------ |
| Select a locked Android device                           | Drizz refuses the connection. Unlock the device first        |
| Install a simulator-only build on a physical iPhone      | Drizz warns you. The install fails. Use a real-device build  |
| Sideload over an App Store install of the same bundle ID | The install fails. Uninstall the store version first         |
| Run a free Apple developer team beyond 7 days            | The provisioning profile expires and the app stops launching |

## Next

* [Set up a device](/desktop-app/device-setup-wizard)
* [Devices: local, cloud & private](/running-tests/devices)
* [Known limitations](/reference/known-limitations)

***

*Last updated: 6 August 2026*


# Use cases

The scenarios teams automate with Drizz — onboarding, authentication, search, checkout, navigation and full regression suites.

|                  |                                                                                      |
| ---------------- | ------------------------------------------------------------------------------------ |
| **Platforms**    | Android  · iOS  · Mobile web                                                         |
| **Best for**     | End-to-end functional flows,                                                         |
| **Also handles** | Cross-app flows, location-dependent flows, API-seeded data                           |
| **Watch out**    | Secure screens — payment PIN entry renders black to any capture tool, Drizz included |

## Prerequisites

* Drizz desktop app installed and signed in
* The app under test registered
* A `login_module` module, for the checkout example below — see [Modules](/writing-tests/modules)

## Scenarios

<table data-search="false"><thead><tr><th>Scenario</th><th>Covers</th></tr></thead><tbody><tr><td><strong>User onboarding</strong></td><td>First launch, permission prompts, profile setup, tutorial skips</td></tr><tr><td><strong>Authentication</strong></td><td>Login, OTP, social sign-in, logout, session expiry</td></tr><tr><td><strong>Search &#x26; filter</strong></td><td>Query entry, result lists, sort and filter combinations, empty states</td></tr><tr><td><strong>Product &#x26; catalog</strong></td><td>Category browsing, product detail, variants, wishlists</td></tr><tr><td><strong>Checkout &#x26; payment</strong></td><td>Cart, address, coupons, order summary, confirmation</td></tr><tr><td><strong>App navigation</strong></td><td>Tab switching, deep links, back behavior, state after backgrounding</td></tr><tr><td><strong>Regression suites</strong></td><td>All of the above, across devices, on every release</td></tr></tbody></table>

Each is an ordinary Drizz test. None requires a different tool or a special mode.

## Copy this

A checkout flow against ShopEase, the demo app used throughout these docs.

```
# ShopEase — add to cart and reach checkout

CALL login_module

Tap on the search icon
Type running shoes in the search field
Tap on the first search result
Wait Until 3 Seconds

Store the price as listed_price
Tap on Add to Cart
Wait Until 2 Seconds

Validate that Item added to cart is visible

Tap on the cart icon
Wait Until 2 Seconds

Validate that the total matches {{listed_price}}
Tap on Proceed to Checkout
Wait Until 3 Seconds

Validate that the checkout page is visible

CLEAR_APP com.shopease.android
```

## Beyond a single app

| Flows                             | Action items                                                                                                                            |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Flows that leave your app**     | An OTP arriving in the SMS app, a payment handoff, an OAuth screen in a browser                                                         |
| **Location-dependent flows**      | Set a location with `SET_GPS(latitude=, longitude=)` before the app reads it                                                            |
| **Data-seeded flows**             | Call your own API mid-test to create the state a scenario needs, then assert the UI matches — see [API steps](/writing-tests/api-steps) |
| **Many accounts or environments** | One script, one [dataset](/writing-tests/which-variable/datasets) per environment                                                       |

Worked examples of each are in [Recipes](/writing-tests/recipes).

## Common mistakes

| What you do                                          | What happens                                                                                                |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Put a 60-step journey in one test                    | A failure mid-flow is hard to isolate and stabilize. One scenario per test is the pattern these docs assume |
| Assert on a payment PIN screen                       | The screen renders black to any capture tool, Drizz included                                                |
| Automate a device matrix before one device is stable | Every device repeats the same failure                                                                       |

## Next

* [Quickstart: your first test](/start-here/quickstart)
* [Recipes](/writing-tests/recipes)
* [Known limitations](/reference/known-limitations)

***

*Last updated: 6 August 2026*


# Download & install

Download the Drizz desktop app, check your Mac meets the requirements, and install it.

The desktop app is the authoring and debugging surface. It drives a device plugged into or emulated on your own Mac.

{% hint style="info" %}
`Drizz Desktop.dmg` → drag to `/Applications`
{% endhint %}

|                     |                                                                                  |
| ------------------- | -------------------------------------------------------------------------------- |
| **Platforms**       | macOS - Apple Silicon and Intel                                                  |
| **Builds**          | Apple Silicon: `latest-mac-arm64.dmg` · Intel: `latest-mac-x64.dmg`              |
| **Minimum macOS**   | 12 or later                                                                      |
| **Free disk space** | 20–30 GB for Android tooling · 40–50 GB for Xcode and iOS simulators             |
| **Watch out**       | Sign-in is work email only. A personal address is rejected at the sign-in screen |

## Prerequisites

* macOS 12 or later
* Apple Silicon, or 64-bit Intel with virtualization support
* 8 GB memory minimum, 16 GB recommended
* 20–30 GB free disk for Android · 40–50 GB free disk for iOS
* A work email address on your company's domain

## Get the right build

Two builds ship. Apple menu → **About This Mac** reports the chip.

| Your Mac                       | Build                  | Download                                                 |
| ------------------------------ | ---------------------- | -------------------------------------------------------- |
| Apple Silicon (M1, M2, M3, M4) | `latest-mac-arm64.dmg` | [`DOWNLOAD`](https://www.drizz.dev/download-desktop-app) |
| Intel                          | `latest-mac-x64.dmg`   | [`DOWNLOAD`](https://www.drizz.dev/download-desktop-app) |

## Install

{% tabs %}
{% tab title="Apple Silicon" %}

1. [Download](https://www.drizz.dev/download-desktop-app).
2. Open the `.dmg`.
3. Drag **Drizz Desktop** into `/Applications`.
4. Launch it from `/Applications` — not from the mounted disk image.
5. Sign in with your work email. See [Sign in & get access](/desktop-app/sign-in-and-access).
6. Confirm the app opens on the Connect Device screen.
   {% endtab %}

{% tab title="Intel" %}

1. [Download](https://www.drizz.dev/download-desktop-app).
2. Open the `.dmg`.
3. Drag **Drizz Desktop** into `/Applications`.
4. Launch it from `/Applications` — not from the mounted disk image.
5. Sign in with your work email. See [Sign in & get access](/desktop-app/sign-in-and-access).
6. Confirm the app opens on the Connect Device screen.
   {% endtab %}
   {% endtabs %}

Note: The arm64 build on an Apple Silicon Mac runs natively. The x64 build on an Apple Silicon Mac runs under translation, and emulator performance drops.

## System requirements

These cover the desktop app plus the Apple and Google tooling it drives. The tooling accounts for the disk and memory figures; the app itself is small.

| Component        | Requirement                                               | Why                                                       |
| ---------------- | --------------------------------------------------------- | --------------------------------------------------------- |
| Operating system | macOS 12 or later                                         | Required by the Xcode versions Drizz supports             |
| Processor        | Apple Silicon or 64-bit Intel with virtualization support | Android emulators need virtualization                     |
| Memory           | 8 GB minimum, 16 GB recommended                           | Emulators, simulators and builds are memory-hungry        |
| Storage          | 20–30 GB free for Android · 40–50 GB free for iOS         | SDK packages, system images, Xcode and simulator runtimes |
| Network          | Stable connection                                         | SDK downloads, sign-in, device provisioning               |

## Windows support

Every shipped desktop build to date has been macOS, in the two variants above. Windows is not supported.&#x20;

The desktop app is not required to run tests on cloud. Drizz Cloud runs test plans in a browser, on any operating system.

## Set up a device

A device is required before a test can run. The [guided setup wizard](/desktop-app/device-setup-wizard) installs and configures the Android and Apple tooling, and is the supported path. Open the Connect Device screen with nothing connected to start it.

## Common mistakes

| What you do                                                       | What happens                                                                                               |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| Download the Intel build on an Apple Silicon Mac                  | It runs under translation, slowly, and emulator performance is poor. Reinstall the arm64 build             |
| Run Drizz from the mounted disk image                             | Updates fail, because the app can't replace itself on a read-only volume. Drag it to `/Applications` first |
| Sign in with a personal Gmail or Outlook address                  | Rejected at sign-in. Use your work email                                                                   |
| Install with 10 GB free                                           | The Android system image download fails partway through step 7 of the wizard. Free up space first          |
| Expect the app to work with no Android or Apple tooling installed | There's nothing to connect to. Run the setup wizard                                                        |

## Next

* [Sign in & get access](/desktop-app/sign-in-and-access)
* [Set up a device: guided wizard](/desktop-app/device-setup-wizard)
* [Networks, proxies & preflight](/desktop-app/networks-and-preflight)

***

*Last updated: 6 August 2026*


# Sign in & get access

Sign in to the desktop app with your work email, how your organization is created, and what to do when you see nothing.

Drizz signs you in with Google SSO on your work email domain and places you in your company's organization automatically.

|                        |                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------- |
| **Sign-in method**     | Google SSO, work email domain only                                                  |
| **Personal addresses** | Blocked                                                                             |
| **Your organization**  | Created automatically by the first person from your domain                          |
| **Trials**             | Enabled by Drizz after your first sign-in, not instantly                            |
| **Watch out**          | Signed in but no apps or files is an access problem, not data loss. Don't reinstall |

## Prerequisites

* Drizz desktop app installed in `/Applications`
* A Google account on your company's email domain
* For trials: an instance enabled by your Drizz contact

## Sign in

1. Launch Drizz Desktop from `/Applications`.
2. Click **Sign in with Google**.
3. Select your work account in the browser window that opens.
4. Return to the app.
5. Confirm you land on the Connect Device screen.

Tokens are stored in the macOS keychain. Signing out clears them.

## Account requirements

<table><thead><tr><th width="374">Requirement</th><th>Detail</th></tr></thead><tbody><tr><td>Email domain</td><td>Your company's work domain. Personal domains are blocked at sign-in</td></tr><tr><td>Domain mismatch</td><td>If your company signs in to Google with a different domain than the one on your business cards, use the Google one</td></tr><tr><td>Account type</td><td>Organization accounts only. There is no personal tier</td></tr></tbody></table>

## How your organization is created

The first person from an email domain to sign in creates the organization. Everyone from that domain who signs in afterwards joins it. No configuration is required.

Everything is org-scoped and shared with colleagues in the same organization:

* Tests, modules and folders
* Datasets
* Memory — app context and blocker rules
* Registered apps and test plans
* Reports
* Your token wallet

## Trials need approval

On a trial, Drizz enables your instance after your first sign-in. Sign-in succeeding is not the same as access being live. The first-day sequence is: install, sign in, then wait for your Drizz contact to confirm the instance is enabled.

## Signed in but you see nothing

An empty app after a successful sign-in is an access problem, not data loss. Files are not stored in the app, and reinstalling changes nothing.

| What you're seeing                           | Cause                 | Fix                                                                                    |
| -------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------- |
| No apps and no files, first day              | Trial not yet enabled | Contact your Drizz representative                                                      |
| Sign-in completes but the app doesn't log in | Known issue           | Quit and relaunch. If it persists, use [Report an issue](/desktop-app/report-an-issue) |

## Common mistakes

| What you do                                             | What happens                                                                                          |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Sign in with a personal address                         | Blocked. There's no personal tier                                                                     |
| Reinstall the app when you see an empty file list       | Nothing changes. The list is empty because of access, not the install                                 |
| Assume your account is live the moment sign-in succeeds | On a trial, access is enabled separately. Check with your Drizz contact                               |
| Share one account across the team                       | Everyone is in the same org anyway — sign in with your own email so reports and runs are attributable |

## Next

* [Set up a device: guided wizard](/desktop-app/device-setup-wizard)
* [Networks, proxies & preflight](/desktop-app/networks-and-preflight)
* [Report an issue](/desktop-app/report-an-issue)

***

*Last updated: 6 August 2026*


# Updates

How the desktop app updates itself, what a forced update does, and how to install one by hand if the download fails.

The desktop app updates itself. Optional updates are taken on demand; forced updates install before work continues.

|                      |                                                                                 |
| -------------------- | ------------------------------------------------------------------------------- |
| **Platforms**        | macOS                                                                           |
| **How it checks**    | Automatically in the background, while the app is open                          |
| **Where you see it** | An update prompt in the sidebar                                                 |
| **Two kinds**        | Optional — take it when you like · Forced — installs before you can continue    |
| **Watch out**        | A forced update restarts the app. Save your open test file before you start one |

## Prerequisites

* Drizz desktop app running from `/Applications`, not from a mounted `.dmg`
* Network access to the Drizz download host
* No unsaved test files open

## Update types

{% tabs %}
{% tab title="Optional" %}

1. Read the update prompt in the sidebar.
2. Click the prompt to download and install, or ignore it and keep working.
3. Confirm the app restarts on the new version.

Ignoring the prompt leaves the current version running. The app has no changelog surface, so a skipped update is the first thing to rule out when behavior changes.
{% endtab %}

{% tab title="Forced" %}

1. A full-screen overlay with a progress bar appears.
2. The download and install run to completion.
3. The app restarts itself.

A forced update cannot be deferred, and it restarts the app mid-authoring. Save the open file when the overlay appears. Forced releases carry fixes for defects that break runs — a broken device connection, a failing sign-in.
{% endtab %}
{% endtabs %}

## When the automatic download fails

A flaky network or a corporate proxy blocking the download host stops the automatic download. The overlay offers a manual installer download.

1. Click the [manual installer link](https://www.drizz.dev/download-desktop-app).
2. Open the downloaded `.dmg`.
3. Drag **Drizz Desktop** into `/Applications`, replacing the existing copy.
4. Launch the app from `/Applications` and confirm the new version loads.

If the download is unreachable from your network, run **Help → Network Preflight** first. Blocked update downloads and blocked sign-in have the same causes. See [Networks, proxies & preflight](/desktop-app/networks-and-preflight).

## Common mistakes

| What you do                                                  | What happens                                                                     |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| Run Drizz from the mounted `.dmg` instead of `/Applications` | Updates fail silently — the app can't replace itself on a read-only volume       |
| Leave an unsaved test file open for days                     | A forced update restarts the app underneath you. Save your work                  |
| Ignore the sidebar prompt for weeks, then report a bug       | You may be reporting something already fixed. Update first, then report          |
| Quit the app during a forced update                          | You'll get the overlay again on next launch. Let it finish                       |
| Reinstall from an old `.dmg` you kept                        | You go backwards, and the next forced update immediately pulls you forward again |

## Next

* [Download & install](/desktop-app/download-and-install)
* [Networks, proxies & preflight](/desktop-app/networks-and-preflight)
* [Report an issue](/desktop-app/report-an-issue)

***

*Last updated: 6 August 2026*


# Set up a device: Guided wizard

The guided wizard installs and configures everything needed to run an Android emulator or an iOS simulator on your Mac.

The guided wizard is the supported setup path. It installs the Android or Apple tooling, creates a device, and boots it.

{% hint style="info" %}
`Connect Device` → nothing connected → **Set up a device**
{% endhint %}

|                  |                                                                           |
| ---------------- | ------------------------------------------------------------------------- |
| **Platforms**    | Android · iOS                                                             |
| **What you get** | A booted Android emulator, or a booted iOS simulator                      |
| **First run**    | 5–10 minutes, most of it one download                                     |
| **Approval**     | Every shell command is shown to you and waits for Approve or Skip         |
| **Watch out**    | Safe to re-run any number of times — already-installed things are skipped |

## Prerequisites

* Drizz desktop app installed and signed in
* macOS 12 or later
* Homebrew installed
* Java from the Homebrew `openjdk@17` package (Android)
* Xcode installed from the Mac App Store and opened once (iOS)
* 20–30 GB free disk for Android · 40–50 GB free disk for iOS

## Start the wizard

1. Open the desktop app.
2. Go to **Connect Device**.
3. Select the platform — **Android** or **iOS**.
4. Click **Set up a device**. With no emulator or simulator present, Drizz offers the guided setup instead of an empty device list.
5. Approve or skip each command as it is presented.
6. Confirm the wizard finishes on a booted device listed in the device picker.

The manual guides — [Android setup](/desktop-app/android-setup) and [iOS simulator setup](/desktop-app/ios-simulator-setup) — cover the cases where the wizard fails, or where a configuration it doesn't create is required.

## Approval and risk badges

Each step displays the exact shell command it will run, with a risk badge, and waits for **Approve** or **Skip**. Nothing runs unapproved. Across a full run the wizard installs software, edits `~/.zshrc`, and downloads several gigabytes.

| Badge        | Means                                                                                            |
| ------------ | ------------------------------------------------------------------------------------------------ |
| **safe**     | Reads state, or installs into a Drizz-managed path. Nothing on your machine changes meaningfully |
| **moderate** | Installs software, or downloads several gigabytes                                                |
| **elevated** | Changes your shell environment — writing `ANDROID_HOME` and `PATH` into `~/.zshrc`               |

**Skip** passes over any step you would rather run yourself, or that is already set up in a non-standard location. The wizard continues from the next step.

## Re-running

Step 1 of every wizard is a diagnostic of what is already present. Anything already installed is skipped silently, and only missing items are presented for approval. Re-run the wizard after a failed step or an unwanted skip.

## Platform flows

{% tabs %}
{% tab title="Android" %}
Ten steps, ending with a booted emulator.

| #  | Step           | What it does                                                                               |
| -- | -------------- | ------------------------------------------------------------------------------------------ |
| 1  | Check          | Diagnoses what's already present — Android Studio, adb, the SDK, the system image          |
| 2  | Android Studio | Installs Android Studio with Homebrew                                                      |
| 3  | adb            | Installs the Android platform tools and links `adb` into the SDK path the emulator expects |
| 4  | SDK tools      | Downloads the command-line tools — `sdkmanager` and `avdmanager`                           |
| 5  | Env vars       | Writes `ANDROID_HOME` and `PATH` into `~/.zshrc`                                           |
| 6  | Licenses       | Accepts the Android SDK licenses                                                           |
| 7  | System image   | Downloads the Android 34 Google Play ARM64 image — about 1.8 GB, and the slow step         |
| 8  | Create AVD     | Creates an emulator called **DrizzPhone** on a Pixel 7 profile                             |
| 9  | Launch         | Starts the emulator in the background                                                      |
| 10 | Wait           | Waits for the emulator to finish booting                                                   |

A first run takes 15–30 minutes, almost all of it in step 7. Later runs take seconds, because steps 2 through 8 are skipped.

The emulator is named **DrizzPhone**, which identifies it in Android Studio's device list and in `adb devices` output.
{% endtab %}

{% tab title="iOS" %}
Two phases. Xcode cannot be downloaded programmatically — it ships through the Mac App Store or the Apple Developer Portal, and neither has a public download URL.

**Phase 1 — download gate.**

1. Click the wizard's link, which opens the App Store on the Xcode page.
2. Install Xcode.
3. Open Xcode once and dismiss the license screen.
4. Click **I've installed & opened Xcode** in the wizard.
5. Confirm the check passes. Drizz verifies that `/Applications/Xcode.app` exists. The command-line tools alone do not pass this check.

**Phase 2 — setup wizard.** Seven steps on the same approve/skip flow as Android, ending with a booted simulator.

Opening Xcode once is required because accepting the Xcode license from the command line only works after the first-launch license screen has been dismissed in Xcode's own window. Otherwise the license step waits for a click that cannot arrive in a background process. Creating a project is not required.

Simulator names come from Apple's tooling — iPhone 17, iPhone Air and so on. Drizz cannot rename them, so there is no iOS equivalent of DrizzPhone. The wizard boots the first available shut-down iPhone.
{% endtab %}
{% endtabs %}

## Known limitations

### Java must be in the Homebrew location

The SDK steps expect Java at the Homebrew `openjdk@17` path. Java installed elsewhere makes steps 4, 6, 7 and 8 fail. Install Java through Homebrew, or skip those steps and run them from a terminal that has your Java on its `PATH`.

### The license step can hang

Clicking "I've installed & opened Xcode" without opening Xcode starts a license step that never finishes. Quit the wizard, open Xcode, dismiss the license screen, and re-run.

### Android Studio may not land in `/Applications`

A known Homebrew cask behavior on first install. Reinstall the cask, then re-run the wizard:

```bash
brew reinstall --cask android-studio
```

### An emulator that crashes after launch fails quietly

Step 10 times out without reporting a cause. Read the emulator's own log:

```bash
cat /tmp/emulator.log
```

## Common mistakes

| What you do                                                    | What happens                                                                       |
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Follow a manual Android Studio guide first                     | You'll do 20 minutes of work the wizard does for you, in paths it won't recognize  |
| Approve step 7                                                 | A 1.8 GB download over a bad connection. Start it somewhere with real bandwidth    |
| Skip step 5 because you don't want `~/.zshrc` edited           | Drizz still works, but `adb` and `emulator` won't be on your own terminal's `PATH` |
| Click "I've installed & opened Xcode" after only installing it | The license step hangs. Open Xcode once, then re-run                               |
| Re-run the wizard from scratch after one failed step           | Fine, and expected. It skips everything already done                               |
| Assume a timed-out step 10 means the wizard is broken          | The emulator crashed. Read `/tmp/emulator.log`                                     |

## Next

* [Android setup (manual)](/desktop-app/android-setup)
* [iOS simulator setup](/desktop-app/ios-simulator-setup)
* [Physical iOS device setup](/desktop-app/ios-physical-device)

***

*Last updated: 6 August 2026*


# Android setup

Set up an Android emulator and the command-line tools by hand, when the guided wizard isn't the right path.

Install the Android tooling manually, create an emulator, and put `adb` on your `PATH`.

|                      |                                                                                                     |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| **Platforms**        | Android                                                                                             |
| **Use this when**    | The [guided wizard](/desktop-app/device-setup-wizard) failed, or you need a specific device profile |
| **You need on PATH** | `adb`, `emulator`, `sdkmanager`, `avdmanager`                                                       |
| **Disk space**       | 20–30 GB                                                                                            |
| **Watch out**        | Disable the emulator's soft keyboard, or taps after typing will fail                                |

## Prerequisites

* Drizz desktop app installed and signed in
* macOS 12 or later
* 20–30 GB free disk space
* Java — the Homebrew `openjdk@17` package is the configuration Drizz is tested against
* An Android SDK location you can write to

{% hint style="success" %}
The [guided setup wizard](/desktop-app/device-setup-wizard) performs everything on this page, shows every command before it runs, and takes 15–30 minutes unattended.
{% endhint %}

## Copy this

Run these three commands once the emulator is running. They are the verification plus the required soft-keyboard fix.

```bash
# 1. Confirm your Mac can see the emulator
adb devices

# 2. Confirm the emulator binary is on your PATH
emulator -list-avds

# 3. Disable the soft keyboard — do this on every emulator you create
adb shell ime disable com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME
adb shell settings put global show_ime_with_hard_keyboard 0
```

`adb devices` lists a working emulator as `device`. `unauthorized`, `offline`, or an empty list means Drizz cannot see it either.

## Do you need the full Android Studio install?

Drizz requires the Android SDK, an emulator system image, and the command-line tools. Android Studio supplies all three, and is the route the guided wizard takes.

Both paths produce the same outcome: a running emulator that `adb devices` can see.

{% tabs %}
{% tab title="Path A — Android Studio" %}
The heavier install, with a GUI for creating and managing emulators.

1. Download Android Studio from [developer.android.com/studio](https://developer.android.com/studio), matching your Mac's chip.
2. Run the installer and complete first-run setup, granting the permissions it asks for.
3. Open **Device Manager** and [create a virtual device](https://developer.android.com/studio/run/managing-avds). A recent Pixel profile on Android 34 with Google Play matches what the wizard creates.
4. Start the emulator and wait for it to finish booting.
5. Run `adb devices` and confirm the emulator lists as `device`.

Android Studio installs the emulator binaries, but not everything Drizz needs on your `PATH`. Continue with **Install the command-line tools** below.
{% endtab %}

{% tab title="Path B — Command-line tools only" %}
A smaller install with no GUI. Emulators are created and launched from a terminal.

1. Install the SDK command-line tools, the platform tools and a system image.
2. Create an AVD with `avdmanager`.
3. Launch it with `emulator`.
4. Run `emulator -list-avds` and confirm the AVD is listed.
5. Run `adb devices` and confirm the emulator lists as `device`.

Java is required. The Homebrew `openjdk@17` package is the configuration Drizz is tested against.
{% endtab %}
{% endtabs %}

## Install the command-line tools

Android Studio installs the emulator binaries by default. `avdmanager`, `sdkmanager` and running `emulator` from any terminal need the following steps.

### 1. Install the SDK components

1. Open **Android Studio → Settings** (Preferences on older macOS).
2. Go to **Languages & Frameworks → Android SDK**.
3. Open the **SDK Tools** tab.
4. Enable **Android Emulator**, **Android SDK Platform-Tools** and **Android SDK Command-line Tools**.
5. Click **Apply**.
6. Note the **Android SDK Location** shown at the top of that screen. The next step needs it.

### 2. Add them to your PATH

All three directories live inside your SDK location.

| Tool                       | Directory                                 |
| -------------------------- | ----------------------------------------- |
| `emulator`                 | `<sdk_location>/emulator`                 |
| `adb`                      | `<sdk_location>/platform-tools`           |
| `sdkmanager`, `avdmanager` | `<sdk_location>/cmdline-tools/latest/bin` |

1. Add the exports to `~/.zshrc`:

   ```bash
   # Replace <sdk_location> with the path shown in Android Studio's SDK Manager
   export ANDROID_HOME="<sdk_location>"
   export PATH="$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
   ```
2. Restart your terminal.
3. Run `emulator -list-avds`. A list of AVDs confirms the change applied; `command not found` means it has not.

Use the `<sdk_location>/emulator` directory. The older copy inside `/tools` is deprecated and does not support current features.

## Disable the soft keyboard

Required on every emulator you create. The on-screen keyboard covers elements, which makes the tap after a `Type` step fail as undoable.

```bash
adb shell ime disable com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME
adb shell settings put global show_ime_with_hard_keyboard 0
```

## Connect from Drizz

1. In the desktop app, open **Connect Device** and choose **Android**.
2. Select your emulator from the list. Boot it from here if it is not already running.
3. Choose the app under test.
4. Confirm the device panel shows the emulator as connected.

Drizz attaches to an emulator that is already running rather than booting a second one. If two are running, close both and reconnect.

## Common mistakes

| What you do                                  | What happens                                                              |
| -------------------------------------------- | ------------------------------------------------------------------------- |
| Skip the soft-keyboard commands              | Taps immediately after a `Type` step fail as undoable, on random screens  |
| Use the deprecated `/tools/emulator` binary  | Modern emulator features don't work. Use `<sdk_location>/emulator`        |
| Connect with the phone or emulator locked    | Drizz refuses a locked device by design. Unlock it                        |
| Run this whole page before trying the wizard | You do the work manually, and the wizard would have skipped it all anyway |

## Next

* [Set up a device: guided wizard](/desktop-app/device-setup-wizard)
* [Real Android device](/desktop-app/android-real-device)
* [Report an issue](/desktop-app/report-an-issue)

***

*Last updated: 6 August 2026*


# Android real device

Connect a real Android phone or tablet over USB — developer options, USB debugging, and the adb check that answers most problems.

Enable USB debugging, connect the device by USB, confirm `adb` sees it, unlock it, connect from Drizz.

|                |                                                                             |
| -------------- | --------------------------------------------------------------------------- |
| **Platforms**  | Android (physical device, over USB)                                         |
| **Requires**   | Developer options and USB debugging enabled on the phone                    |
| **The check**  | `adb devices` must list it as `device`                                      |
| **Lock state** | The device must be **unlocked**. Drizz refuses a locked device              |
| **Watch out**  | `unauthorized` means the debugging prompt on the phone hasn't been accepted |

## Prerequisites

* Drizz desktop app installed and signed in
* An Android phone or tablet
* A data-capable USB cable — charge-only cables carry no data
* `adb` on your `PATH` — see [Android setup](/desktop-app/android-setup)
* A raised screen timeout on the device

## Copy this

```bash
# Should print your device's serial followed by the word "device"
adb devices

# If it's missing entirely, restart the adb server and check again
adb kill-server && adb start-server
adb devices
```

## Set it up

1. Enable **Developer options** on the phone: Settings → About phone → tap **Build number** seven times. The menu path varies by manufacturer.
2. Enable **USB debugging** in Settings → System → Developer options.
3. Connect the phone to your Mac by USB.
4. Accept the "Allow USB debugging?" prompt on the phone, and tick **Always allow from this computer**.
5. Run `adb devices` and confirm the phone lists as `device`, not `unauthorized`.
6. Unlock the phone. Drizz checks lock state before connecting and refuses a locked device.
7. In the desktop app, open **Connect Device** and select your phone.
8. Choose the app under test.
9. Confirm the device panel shows the phone as connected.

## Reading `adb devices`

| Output                       | Cause                                     | Fix                                                                          |
| ---------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------- |
| `<serial> device`            | Ready                                     | Connect from Drizz                                                           |
| `<serial> unauthorized`      | The debugging prompt hasn't been accepted | Unlock the phone and accept it. Tick "always allow"                          |
| `<serial> offline`           | adb has lost the connection               | `adb kill-server && adb start-server`, or replug                             |
| Nothing listed               | The Mac can't see the device at all       | Try another cable — many charge-only cables carry no data. Then another port |
| A device you don't recognize | A running emulator                        | No action. Emulators and physical devices both appear in the list            |

Keep the device unlocked and plugged in for the whole run. A screen lock mid-run ends the session.

## Disable the soft keyboard

Required on physical devices as well as emulators. An open keyboard covers the next element and makes the tap after a `Type` step fail.

```bash
adb shell ime disable com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME
adb shell settings put global show_ime_with_hard_keyboard 0
```

For a phone that ships a different keyboard, substitute its package name.

## Common mistakes

| What you do                                       | What happens                                                                    |
| ------------------------------------------------- | ------------------------------------------------------------------------------- |
| Use a charge-only USB cable                       | The device never appears in `adb devices`. Nothing about the error says "cable" |
| Miss the debugging prompt on the phone            | `unauthorized`. Unlock, replug, and accept it                                   |
| Leave the screen locked                           | Drizz refuses to connect. Unlock before connecting                              |
| Let the screen lock mid-run                       | The run dies partway. Raise the screen timeout on your test device              |
| Forget to turn off the soft keyboard              | Taps right after a `Type` step fail as undoable                                 |
| Assume Drizz is broken when the device is missing | Run `adb devices` first. If adb can't see it, Drizz can't either                |

## Next

* [Android setup (manual)](/desktop-app/android-setup)
* [Set up a device: guided wizard](/desktop-app/device-setup-wizard)
* [Report an issue](/desktop-app/report-an-issue)

***

*Last updated: 6 August 2026*


# iOS simulator setup

Install Xcode, open it once, launch a simulator, and connect it to Drizz. What happens on the first connect.

Install Xcode, open it once, boot a simulator, connect from the desktop app.

{% hint style="info" %}
`Connect Device` → **iOS** → pick a simulator
{% endhint %}

|                   |                                                                                  |
| ----------------- | -------------------------------------------------------------------------------- |
| **Platforms**     | iOS (simulator)                                                                  |
| **Requires**      | Xcode, installed from the App Store and **opened once**                          |
| **Disk space**    | 40–50 GB                                                                         |
| **First connect** | A minute or two longer — Drizz builds WebDriverAgent, then caches it             |
| **Watch out**     | Installing Xcode isn't enough. If you never open it, the simulator won't connect |

## Prerequisites

* Drizz desktop app installed and signed in
* macOS 12 or later
* **Xcode**, installed from the Mac App Store and opened at least once with the license screen dismissed
* Xcode command-line tools installed
* 40–50 GB free disk space

{% hint style="success" %}
The [guided setup wizard](/desktop-app/device-setup-wizard) performs everything on this page except the Xcode download, which Apple does not allow any app to automate.
{% endhint %}

## Set up Xcode

1. Install **Xcode** from the Mac App Store.
2. Open Xcode once and dismiss the license screen. Creating a project is not required.
3. Install the command-line tools when Xcode prompts on first launch.
4. Confirm `/Applications/Xcode.app` exists. Drizz checks for the full app, not the command-line tools.

Opening Xcode once is required because Drizz accepts the Xcode license from the command line during setup. That only works after the first-launch license screen has been dismissed in Xcode's own window. Otherwise the command waits for a click that cannot arrive in a background process, and it hangs.

## Boot a simulator

1. Open Xcode → **Open Developer Tool** → **Simulator**.
2. Select any iPhone simulator.
3. Wait for it to finish booting, and leave it running.

Drizz connects to a booted simulator. Outside the guided wizard, it does not install one.

## Connect from Drizz

1. In the desktop app, open **Connect Device** and choose **iOS**.
2. Select your simulator from the list.
3. Choose the app under test.
4. Confirm the device panel shows the simulator as connected.

## First connect

Drizz builds and installs **WebDriverAgent** onto the simulator on the first connect. That takes one to two minutes.

| Behavior      | Detail                                                                     |
| ------------- | -------------------------------------------------------------------------- |
| Build caching | The build is cached and reused, so later connects are fast                 |
| Pre-build     | Drizz pre-builds WebDriverAgent in the background when the app starts      |
| Apparent hang | A first connect can take up to two minutes before the device panel updates |

## Simulator vs physical device

| Capability                    | Simulator              | Physical device                                    |
| ----------------------------- | ---------------------- | -------------------------------------------------- |
| Authoring and debugging       | ✅                      | ✅                                                  |
| Camera, Bluetooth, biometrics | ❌                      | ✅                                                  |
| Real push notifications       | ✅                      | ✅                                                  |
| Hardware-only app builds      | ❌                      | ✅                                                  |
| Cost and availability         | Free, always available | Requires an Apple development team and a USB cable |

Setup for hardware is on [Physical iOS device setup](/desktop-app/ios-physical-device).

iOS steps run approximately twice as slow as the equivalent Android steps. This is a platform characteristic, not a property of the test.

## Common mistakes

| What you do                                   | What happens                                                                      |
| --------------------------------------------- | --------------------------------------------------------------------------------- |
| Install Xcode but never open it               | Setup hangs on the license step, and the simulator won't connect                  |
| Install only the command-line tools           | Drizz checks for the full `Xcode.app` and refuses. Install Xcode itself           |
| Assume the first connect is stuck             | It's building WebDriverAgent. Wait two minutes before restarting anything         |
| Quit the simulator between runs               | Every connect starts from cold. Leave it booted while you're authoring            |
| Rely on `CLEAR_APP` for a clean state on iOS  | It's effectively a no-op on iOS. Reset in-app, or reinstall between tests         |
| Use `PRESS_DEVICE_BACK_BUTTON` in an iOS test | The command doesn't exist on iOS. Tap the back button or swipe from the left edge |

## Next

* [Physical iOS device setup](/desktop-app/ios-physical-device)
* [Set up a device: guided wizard](/desktop-app/device-setup-wizard)
* [Tour of the interface](/desktop-app/interface-tour)

***

*Last updated: 6 August 2026*


# iOS physical device

Connect a real iPhone or iPad — developer mode, the Apple team picker, signing, and the free-account limits.

Real iPhones and iPads are supported from desktop app v1.8.0, on both Apple Silicon and Intel Macs.

{% hint style="info" %}
`Connect Device` → **iOS** → your device → pick your Apple team
{% endhint %}

|                     |                                                                                |
| ------------------- | ------------------------------------------------------------------------------ |
| **Platforms**       | iOS (physical device, from v1.8.0)                                             |
| **Requires**        | Xcode, a USB cable, Developer Mode on the device, an Apple developer team      |
| **Free Apple team** | Profiles expire after **7 days**, and there's a cap on sideloaded apps         |
| **App build**       | Must be a **real-device build** — a simulator build won't install              |
| **Watch out**       | You have to trust the developer certificate on the device before anything runs |

## Prerequisites

* Drizz desktop app v1.8.0 or later, signed in
* Xcode installed and opened once — see [iOS simulator setup](/desktop-app/ios-simulator-setup)
* An Apple development team, free or paid
* An iPhone or iPad, unlocked, with a USB cable
* A real-device build of the app under test

## Set up the device

1. Install Xcode on your Mac and open it once. See [iOS simulator setup](/desktop-app/ios-simulator-setup).
2. Connect the device by USB.
3. Tap **Trust** when the device asks whether to trust this computer.
4. Enable **Developer Mode** on the device: Settings → Privacy & Security → Developer Mode.
5. Restart the device when prompted. Developer Mode does not take effect until the restart.
6. In Drizz Desktop, open **Connect Device** and select your device.
7. Select your Apple development team in the team picker.
8. Click connect. Drizz signs WebDriverAgent and installs it onto the device.
9. Trust the developer certificate on the device: Settings → General → VPN & Device Management → your certificate → **Trust**.
10. Confirm the device panel shows the device as connected.

Until the certificate is trusted at step 9, iOS refuses to launch anything Drizz installed, and the connection fails with a signing error that does not name the Trust step.

## Apple team types

Drizz labels free and paid teams in the picker and warns when a free team is selected. These limits are Apple's, not Drizz's.

| Limit                         | Free team                                           | Paid team                                 |
| ----------------------------- | --------------------------------------------------- | ----------------------------------------- |
| Provisioning profile lifetime | **7 days**, then re-sign by reconnecting from Drizz | Standard Apple Developer Program duration |
| Sideloaded app cap            | Capped — reached quickly on a shared test device    | Higher                                    |
| Paid-account entitlements     | Unavailable                                         | Available                                 |

## Installing your app

| Rule                                                              | Consequence                                                                                                          |
| ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| The build must be a real-device build                             | A simulator build will not install on hardware. Drizz names this reason in the error rather than failing generically |
| An App Store copy blocks a sideloaded build of the same bundle ID | iOS will not let a self-signed build replace an App Store install. Uninstall the store version from the device first |

iOS test plans on the Drizz device cloud also require a real-device build.

## Common mistakes

| What you do                                        | What happens                                                                                    |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Skip the Trust step on the device                  | The connection fails with a signing error. Settings → General → VPN & Device Management → Trust |
| Enable Developer Mode but not restart              | Developer Mode doesn't take effect until the restart                                            |
| Use a simulator build on hardware                  | It won't install. Drizz names the reason — build for a real device                              |
| Sideload over an App Store install of the same app | Blocked by iOS. Uninstall the store version first                                               |
| Use a free Apple team for an ongoing suite         | It breaks every 7 days when the profile expires. Use a paid account                             |
| Unplug the device mid-run                          | The session dies. Keep it connected and unlocked for the whole run                              |
| Leave the device locked                            | Drizz checks lock state and refuses a locked device                                             |

## Next

* [iOS simulator setup](/desktop-app/ios-simulator-setup)
* [Real Android device](/desktop-app/android-real-device)
* [Report an issue](/desktop-app/report-an-issue)

***

*Last updated: 6 August 2026*


# Networks, proxies & preflight

Run the network preflight check, read its results, and work with corporate proxies and TLS inspection.

Preflight reports whether the desktop app can reach every service it needs, and names the layer that fails.

{% hint style="info" %}
`Help` → **Network Preflight**
{% endhint %}

|                        |                                                                                                                       |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Platforms**          | macOS                                                                                                                 |
| **When it runs**       | Automatically before device setup, and any time you ask for it                                                        |
| **What "ready" means** | Every check shows **OK** and the connection status reads **Ready**                                                    |
| **Corporate TLS**      | Drizz reads the macOS system certificate store — no configuration needed when the root certificate is installed there |
| **Watch out**          | Works on a hotspot but not on office wifi means TLS inspection or an egress rule, not a Drizz fault                   |

## Prerequisites

* Drizz desktop app installed
* A network connection
* For corporate networks: the company root certificate in the macOS **system** keychain

## Run preflight

1. Open **Help → Network Preflight**.
2. Click **Run**. Drizz also runs this check automatically before desktop setup.
3. Read the **connection status** at the top.
4. Identify the rows that are not **OK**. Preflight names the specific service or network layer that failed.
5. Fix the topmost failing row. A failure high in the list explains the failures below it.
6. Click **Run** to re-execute the check.
7. Confirm every check shows **OK** and the status reads **Ready**, then click **Continue**.

## What it checks

Four layers, in the order they matter.

| Layer                 | What's being confirmed                                                            |
| --------------------- | --------------------------------------------------------------------------------- |
| **Network integrity** | DNS resolution, proxy routing, HTTPS reachability                                 |
| **Sign-in**           | That the identity services behind sign-in are reachable and your session is valid |
| **Platform services** | That the services which allocate devices and store your tests respond             |
| **Live connection**   | That the real-time channel used to stream device state and logs stays open        |

## Corporate networks, proxies and TLS inspection

Drizz reads the **macOS system certificate store**. On a network with a TLS-inspecting proxy, no configuration is required when the company root certificate is installed in the system keychain.

| Symptom                                               | Diagnosis                                                                                                                                                              |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Works on a phone hotspot, fails on the office network | **Cause:** TLS inspection or an egress rule. **Fix:** run preflight on both networks and compare which rows differ. That list goes to IT                               |
| Preflight fails only on the corporate network         | **Cause:** the corporate root certificate is not in the macOS system keychain. **Fix:** install it in the system keychain, not a user keychain or a browser store      |
| Runs start and then die partway                       | **Cause:** a proxy terminating long-lived connections after a fixed idle period. **Fix:** exempt the real-time connection from the idle timeout                        |
| An automatic app update can't download                | **Cause:** the same egress rules that block sign-in. **Fix:** run preflight before treating the update as broken. See [Updates & forced updates](/desktop-app/updates) |

Checks to raise with IT when preflight fails on a corporate network:

* Is the corporate root certificate installed in the **system** keychain, not just a user keychain or a browser store?
* Is HTTPS egress to Drizz's domains allowed, including the real-time connection?
* Is there a proxy that terminates long-lived connections after a fixed idle period?

## Common mistakes

| What you do                                                  | What happens                                                           |
| ------------------------------------------------------------ | ---------------------------------------------------------------------- |
| Debug a failing run for an hour before running preflight     | Preflight would have named the failing layer in 20 seconds             |
| Assume "works on hotspot, not on office wifi" is a Drizz bug | It's TLS inspection or an egress rule. Take the preflight output to IT |
| Install the corporate certificate in a browser only          | Drizz reads the macOS system store. It needs to be there               |
| Re-run preflight without fixing anything                     | Same result. Fix the topmost red row, then re-run                      |
| Ignore an amber real-time connection row                     | Runs start and then die partway. That row is the reason                |
| Reinstall the app to fix a network problem                   | Nothing changes. The network is outside the app                        |

## Next

* [Download & install](/desktop-app/download-and-install)
* [Updates & forced updates](/desktop-app/updates)
* [Report an issue](/desktop-app/report-an-issue)

***

*Last updated: 6 August 2026*


# Tour of the interface

A map of the desktop app window — what each panel does and what each control means.

The desktop app is one window with seven areas plus a wallet badge.

|               |                                                                     |
| ------------- | ------------------------------------------------------------------- |
| **Platforms** | macOS                                                               |
| **Left**      | Sidebar and file explorer — profile, projects, tests, modules       |
| **Center**    | Editor — your test file, multiple tabs, `/` for the command list    |
| **Right**     | Device panel and variables                                          |
| **Bottom**    | Console — live logs, per-step screenshots, run status               |
| **Watch out** | The wallet badge is the first thing to check when a run won't start |

## Prerequisites

* Drizz desktop app installed and signed in
* A connected device for the device panel and console to populate

## Panels

<table data-search="false"><thead><tr><th>#</th><th>Area</th><th>Purpose</th></tr></thead><tbody><tr><td>1</td><td><strong>Sidebar</strong></td><td>Profile, Discover, a link to these docs, update prompts, and the collapsible <strong>Drizz Studio</strong> panel</td></tr><tr><td>2</td><td><strong>File explorer</strong></td><td>Projects, tests, modules and folders, and the Online / Local toggle</td></tr><tr><td>3–4</td><td><strong>Editor</strong></td><td>The open test file. Multiple tabs, <code>/</code> command hints, autocomplete, per-step status decorators, save and save-as with unsaved-change tracking</td></tr><tr><td>5</td><td><strong>Device panel</strong></td><td>Connected devices, the device picker, app selection, connect and disconnect</td></tr><tr><td>6</td><td><strong>Variables panel</strong></td><td>The variables available to the open file, colored by whether they resolve</td></tr><tr><td>7</td><td><strong>Console</strong></td><td>Live execution log, per-step screenshots, run status banner</td></tr><tr><td>8</td><td><strong>Wallet badge</strong></td><td>DT balance</td></tr></tbody></table>

Two surfaces sit alongside the window layout:

| Surface    | Purpose                                                             |
| ---------- | ------------------------------------------------------------------- |
| **Chat**   | Describe a test in plain English and have Drizz generate the script |
| **Memory** | Org-level context and blocker rules for an app                      |

## Editor controls

| Control              | Behavior                                                                                                                                                        |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/` on an empty line | Opens the command palette with autocomplete. The editor shows the hint *"Type '/' for Drizz supported commands"*. The palette is the authoritative command list |
| Tabs                 | Several test files open at once. The active tab shows an unsaved-change indicator                                                                               |
| Step decorators      | A status marker in the gutter on each line during and after a run — passed, failed, running, not yet reached                                                    |
| Module expansion     | A `CALL` line shows the module's body inlined beneath it, indented and read-only. ***Edits are made in the module file***                                       |
| Command Guidelines   | Explain command level particulars and ready to use command examples. Mentions best practice for all commands.                                                   |

## Device panel

| State             | Behavior                                                                                                            |
| ----------------- | ------------------------------------------------------------------------------------------------------------------- |
| Devices available | Pick a device, pick the app under test, connect. The panel shows the connected device, its OS version and its state |
| No device set up  | The panel offers the [guided setup wizard](/desktop-app/device-setup-wizard)                                        |

## Variables panel

| Color     | Meaning                                                                          |
| --------- | -------------------------------------------------------------------------------- |
| **Green** | The variable resolves against the selected dataset                               |
| **Red**   | The variable does not exist in the selected dataset. That step fails at run time |

## Console

| Element                              | Meaning                                                                                                                  |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| Live log lines                       | Step-by-step execution output for the current run                                                                        |
| Status banner                        | Run status and the run's token usage                                                                                     |
| *"Predicted Action drizzing fast ⚡"* | The step resolved from cache instead of by Vision AI. It ran in a fraction of the time and cost a fraction of the tokens |
| Steps you didn't write               | Automatic popup dismissal, from blocker rules defined in Memory for your app                                             |

## Wallet badge

| Fact                   | Detail                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| Shows                  | Remaining DT balance, and DT used by the current run                                       |
| Vague run-start errors | An exhausted balance can present as a server or availability error. Check this badge first |
| After a top-up         | The badge does not always refresh immediately. Navigate away and back                      |

## Reporting a problem

**Report Issue** bundles logs, recent screenshots and device state and sends them to Drizz support. See [Report an issue](/desktop-app/report-an-issue).

## Common mistakes

| What you do                                  | What happens                                                                    |
| -------------------------------------------- | ------------------------------------------------------------------------------- |
| Hunt through menus for the command list      | Press `/` on an empty line. That palette is the complete list                   |
| Ignore a red variable in the variables panel | The run fails on that step. Fix it before you press run                         |
| Read a failure from the log alone            | The per-step screenshot answers most failures. Open the before-screenshot first |
| Edit the inlined module body under a `CALL`  | It's read-only. Edit the module itself                                          |
| Debug a vague "run won't start" error        | Check the wallet badge before anything else                                     |
| Describe a symptom in an email to support    | Use Report Issue. It sends the logs, screenshots and device state with it       |

## Next

* [Projects, tests & modules](/desktop-app/projects-tests-and-modules)
* [Report an issue](/desktop-app/report-an-issue)
* [Set up a device: guided wizard](/desktop-app/device-setup-wizard)

***

*Last updated: 6 August 2026*


# Projects, tests & modules

How the file explorer organizes work — tests, modules, folders, the green M badge, and the Online/Local toggle.

A project holds two file types — tests and modules — organized into folders in the file explorer.

{% hint style="info" %}
**At a glance**

`New File` → Test · Module · Folder
{% endhint %}

|                        |                                                                                                                                                 |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **File types**         | <p><strong>Test</strong> — one end-to-end flow <br><strong>Module</strong> — reusable steps called from tests</p>                               |
| **Telling them apart** | Modules carry a green **M** badge in the file explorer                                                                                          |
| **Workspace**          | **Online** syncs with your org · **Local** shows drafts on your Mac                                                                             |
| **Scope**              | Everything is org-scoped — your colleagues see the same tests and modules                                                                       |
| **Watch out**          | Deleting a project that holds a module breaks every test that calls it, Drizz warns while taking this action. Resolving modules is the way out. |

## Prerequisites

* Drizz desktop app installed and signed in
* A project in your organization

## File types

<table data-search="false"><thead><tr><th></th><th>Test</th><th>Module</th><th>Folder</th></tr></thead><tbody><tr><td><strong>Contains</strong></td><td>One end-to-end flow, one step per line</td><td>A block of steps called from many tests</td><td>Tests, modules and other folders</td></tr><tr><td><strong>Badge</strong></td><td>None</td><td>Green <strong>M</strong> in the file explorer</td><td>Folder icon</td></tr><tr><td><strong>Edited in</strong></td><td>The editor</td><td>The editor</td><td>—</td></tr><tr><td><strong>Runs</strong></td><td>Locally on a connected device, or in the cloud through a test plan</td><td>Only when called from a test</td><td>Not runnable</td></tr><tr><td><strong>Supports</strong></td><td>Validations, conditionals, scrolling, system commands, module calls</td><td>The same commands as a test</td><td>Expand and collapse</td></tr><tr><td><strong>Called by</strong></td><td>—</td><td><code>CALL &#x3C;module_name></code></td><td>—</td></tr><tr><td><strong>Functional effect of nesting</strong></td><td>None — a test in a folder behaves the same as one at the root</td><td>None</td><td>Organization only</td></tr></tbody></table>

Full module syntax, parameters and nesting rules are on [Modules](/writing-tests/modules).

## Calling a module

```
CALL login_module
```

The module's body appears inlined beneath the call, indented and read-only. Edits are made in the module file, not in the inlined copy.

## Naming conventions

| Item   | Convention                              | Example                                                          |
| ------ | --------------------------------------- | ---------------------------------------------------------------- |
| Test   | The flow's purpose, prefixed with an ID | `T01 — Invalid search feedback`, not `test1`                     |
| Module | What it does, in `snake_case`           | `login_module`, `city_selection_from_homepage`, not `T01_module` |
| Folder | Feature, sprint or team                 | —                                                                |

## Online and Local

| View       | Shows                                                                    |
| ---------- | ------------------------------------------------------------------------ |
| **Online** | Files synced with your organization's project — what your colleagues see |
| **Local**  | Drafts and locally stored files on your own Mac                          |

**Test plans run what is Online. A file left in Local does not run in the cloud.**

## Sharing and module dependencies

Your organization is created automatically from your email domain. Tests, modules, folders, datasets and Memory are all org-scoped and visible to every member.

A module is a dependency of every test that calls it:

| Action                                 | Effect                                                                                                                                     |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `CALL` with no matching module         | The step fails                                                                                                                             |
| Deleting a project that holds a module | Every test calling that module breaks. The warning is easy to miss                                                                         |
| Renaming a module                      | Every `CALL` using the old name gets auto updated, editing modules applies changes to all test where that module will be called in future. |

## Creating a file

1. Click **New File**.
2. Select **Test**, **Module** or **Folder**.
3. Name it per the conventions above.
4. Confirm it appears in the file explorer tree.

## Common mistakes

<table data-search="false"><thead><tr><th>What you do</th><th>What happens</th></tr></thead><tbody><tr><td>Delete a project to tidy up</td><td>Every test calling a module in it breaks, and the warning is easy to miss</td></tr><tr><td>Name tests <code>test1</code>, <code>test2</code></td><td>Unreadable at 40 files. Name them for the flow</td></tr><tr><td>Edit the inlined module body under a <code>CALL</code></td><td>It's read-only. Edit the module file</td></tr><tr><td>Type <code>CALL login_module</code> straight through</td><td>Autocomplete lags. Type <code>CALL</code>, pause for the dropdown, then pick</td></tr><tr><td>Assume your files are private</td><td>Everything is shared with your organization</td></tr><tr><td>Leave work in Local and expect a cloud plan to run it</td><td>Test plans run what's Online. Sync it</td></tr></tbody></table>

## Next

* [Modules](/writing-tests/modules)
* [Tour of the interface](/desktop-app/interface-tour)
* [The shape of a test](/writing-tests/writing-tests)

***

*Last updated: 6 August 2026*


# Report an issue

Report Issue bundles your logs, screenshots and device state and sends them to Drizz support in one click.

**Report Issue** in the desktop app collects the logs, screenshots and device state support needs, and sends them.

{% hint style="info" %}
`Report Issue` — in the desktop app
{% endhint %}

|                    |                                                                                                      |
| ------------------ | ---------------------------------------------------------------------------------------------------- |
| **Platforms**      | macOS                                                                                                |
| **What it sends**  | Only platform related support, device and connection state. For testcase refernece mention thread ID |
| **When to use it** | Anything you can't explain from the run report and the troubleshooting table                         |
| **Why**            | Support gets the evidence immediately instead of asking for it tomorrow                              |
| **Watch out**      | Send it while the problem is fresh — the bundle carries *recent* logs, not all history               |

## Prerequisites

* Drizz desktop app installed and signed in
* The problem reproduced recently — the bundle carries recent logs only
* One line describing what you expected and what happened

## Check these first

These resolve most reports in under a minute.

1. Check the wallet badge. A vague server or availability error can be an exhausted balance.
2. Run **Help → Network Preflight**, especially if the same test worked on another network. See [Networks, proxies & preflight](/desktop-app/networks-and-preflight).
3. Open the before-screenshot of the failing step in report. A screen other than the expected one means the real failure is an earlier step.
4. Confirm you are on the current version. See [Updates & forced updates](/desktop-app/updates).

## Send a report

1. Reproduce the problem, or send the report during an occurrence.
2. Open **Report Issue** in the desktop app.
3. Write one specific line in the description field with Thread ID.
4. Submit. Drizz attaches the logs, screenshots and device state automatically — no manual attachment is required.

## What the bundle contains

| Included                        | Why support needs it                                                                        |
| ------------------------------- | ------------------------------------------------------------------------------------------- |
| **Application logs**            | What the app was doing at the moment it went wrong, including anything that failed silently |
| **Recent screenshots**          | What was actually on the device screen                                                      |
| **Device and connection state** | Which device, which platform, whether it was connected, what state it was in                |
| **App and environment details** | Your version, so support knows whether you're hitting something already fixed               |

The bundle carries **recent** logs and screenshots, not full history. Evidence from a problem hit three days earlier may have rolled off. For an intermittent problem, send the report on an occurrence.

## When to use it

| Situation                                                                                                                   | Use Report Issue |
| --------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| A run fails and neither the run report nor the [troubleshooting table](/reports-and-debugging/troubleshooting) explains why | ✅                |
| Sign-in completes but the app doesn't log you in, and relaunching didn't help                                               | ✅                |
| A device shows up in `adb devices` or Xcode but not in Drizz                                                                | ✅                |
| A step behaves differently from one run to the next with no change to the script                                            | ✅                |
| Anything hangs                                                                                                              | ✅                |

## What to put in the description

The bundle carries the facts. The description carries the intent.

| Include                                | Example                                       |
| -------------------------------------- | --------------------------------------------- |
| What you expected to happen            | The cart screen opens                         |
| What happened instead                  | The tap fails as undoable                     |
| Which test and which step              | T04 step 7 — tap on Add to Cart               |
| Whether it's every run or intermittent | Every run since this morning                  |
| Which platform                         | Fails on the iOS simulator, passes on Android |

One line covering all five: `<Thread ID> T04 step 7 — tap on Add to Cart fails on the iOS simulator, passes on Android, every run since this morning`

## Common mistakes

| What you do                                              | What happens                                                         |
| -------------------------------------------------------- | -------------------------------------------------------------------- |
| Email a description of the symptom                       | Support asks for logs, and you lose a day to the round trip          |
| Use Report Issue three days after the problem            | The relevant logs may have rolled off. Do it while it's live         |
| Send the bundle with no description                      | Support has the evidence but not what you expected to see            |
| Photograph the error with your phone                     | The bundle already contains the real screenshots                     |
| Use it before checking your wallet balance               | A vague server error is often an empty wallet                        |
| Use it for a device problem before running `adb devices` | If adb can't see the device, Drizz can't either — and that's the fix |

## Next

* [Networks, proxies & preflight](/desktop-app/networks-and-preflight)
* [Troubleshooting](/reports-and-debugging/troubleshooting)
* [Updates & forced updates](/desktop-app/updates)

***

*Last updated: 6 August 2026*


# Drizz AI

Describe a flow in plain language and Drizz explores the app and writes the script for you. Android only, best under 25 steps.

Chat takes a plain-language description of a flow, runs it on a live device, works out each step as it goes, and produces a script.

|                   |                                                                 |
| ----------------- | --------------------------------------------------------------- |
| **Platforms**     | Android · iOS                                                   |
| **Where it runs** | A local Android emulator connected to the desktop app           |
| **Best length**   | \~20–25 steps per run                                           |
| **Not generated** | API steps, module calls, `Store` and `SET` — add those yourself |
| **Watch out**     | The output is a first draft, not a finished test                |

Drizz AI is also referred to as Fathom.

## Prerequisites

* Drizz desktop app installed and signed in
* A local Android emulator running
* The emulator connected to the desktop app
* The app under test installed and open on that emulator

## Run a test with Chat

1. Click the chat icon at the top left of the editor.
2. Intent: one or two sentences describing the scenario, including any data the flow needs, such as a phone number, a search term or an address.
3. End Condition: state the condition to end the test execution
4. Watch the run. Drizz executes on the emulator one step at a time, looking at the current screen, deciding the next action and performing it.
5. Answer any question Drizz asks. It pauses on an unexpected screen, a navigation loop or an ambiguous path, and resumes once you reply.
6. Review the generated script, which opens in the editor when the run finishes.

## What gets generated

| Included            | Detail                                                                                                                                   |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| UI actions          | The full sequence performed during the run                                                                                               |
| `IF` blocks         | Where the intent implied conditional behavior, so the script reflects the app's real branching rather than the single path this run took |
| Validation commands | Where checks were specified in the intent or during the run                                                                              |
| Navigation steps    | The steps needed to reach the flow                                                                                                       |

## Edit and save the output

The output is an ordinary Drizz script. Before saving:

1. Tighten any description that reads vaguely against the app's exact wording.
2. Add waits after anything that navigates or calls the network.
3. Add the validations that matter. Generated scripts under-validate.
4. Replace repeated setup with a `CALL` to a shared module.
5. Add any `Store`, `SET` or `API:` steps the flow needs.
6. End the script with `CLEAR_APP`.
7. Save the test and run it once from the editor to confirm it passes unattended.

A saved Chat script behaves like any other test — add it to a test plan, run it on the device cloud, use it for regression.

## Limits

| Limit                       | Detail                                                                                                                                                                                                                       |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| API/Variable/Module support | Only fuctional UI testing is supported                                                                                                                                                                                       |
| Run length                  | \~20–25 steps. Beyond that Drizz loses track of the flow and produces missed steps or inconsistent behavior. Split a long journey into two or three runs and stitch the scripts together, or make the shared prefix a module |
| Not generated               | API steps, module calls, `Store` and `SET`. Add them afterwards                                                                                                                                                              |
| Conventions                 | Chat has no knowledge of your naming, your module library or your validation depth                                                                                                                                           |
| Not suited to               | Long regression journeys, flows needing API setup, and any test that must be exactly right on the first pass                                                                                                                 |

## Common mistakes

| What you write                                    | What happens                                                             |
| ------------------------------------------------- | ------------------------------------------------------------------------ |
| An intent covering a 40-step journey              | Drizz loses context partway. Split it                                    |
| `test the checkout flow`                          | Too vague. Name the screens, the data and the end check                  |
| Expecting `API` `SET` or `CALL` in the output     | Not generated. Add them in the editor                                    |
| Saving the generated script unedited              | Under-validated, and descriptions may not match your app's exact wording |
| Waiting for Chat while it's asking you a question | It pauses for clarification. Answer it and the run resumes               |

## Next

* [The shape of a test](/writing-tests/writing-tests)
* [Modules](/writing-tests/modules)
* [Authoring rules](/writing-tests/authoring-rules)

***

*Last updated: 6 August 2026*


# The shape of a test

The structure of a Drizz test: one instruction per line, five beats, and the command palette that lists everything Drizz understands.

A Drizz test is a plain text file: one instruction per line, running top to bottom.

|                  |                                                              |
| ---------------- | ------------------------------------------------------------ |
| **Format**       | Plain text. No selectors, no page objects, no SDK            |
| **Comments**     | Lines starting with `#` are skipped                          |
| **Command list** | Press `/` on an empty line in the editor                     |
| **Test shape**   | Setup → Navigate → Act → Validate → Clean up                 |
| **Watch out**    | If a command isn't in the `/` palette, Drizz doesn't have it |

## Prerequisites

* Drizz desktop app installed and signed in
* A connected device or emulator
* A registered app — `com.shopease.android` in these examples
* A project with at least one test file

## Copy this

```
# T04 — Search and add to cart

CALL login_module

Tap on the search icon
Wait Until 2 Seconds

Type running shoes in the search field
Tap on the first search result
Wait Until 3 Seconds

Validate that the product detail page is visible

Store the price as listed_price
Tap on Add to Cart
Wait Until 2 Seconds

Validate that Item added to cart is visible

CLEAR_APP com.shopease.android
```

## Write a test

1. Open a test file in the desktop app.
2. Press `/` on an empty line to list every command Drizz supports.
3. Write one instruction per line, in the app's own wording. If the button reads `Charge Now`, write `Charge Now`.
4. Order the lines Setup → Navigate → Act → Validate → Clean up.
5. Add a `Validate` after each step that navigates, submits or pays.
6. End with `CLEAR_APP <your_package_name>` so the next test starts from first launch.
7. Run the test and confirm every step resolved in the report.

## The five beats

| Beat             | What it does                             | Commands                                             |
| ---------------- | ---------------------------------------- | ---------------------------------------------------- |
| **1 · Setup**    | Open the app and get to a known state    | `CALL login_module`, `OPEN_APP com.shopease.android` |
| **2 · Navigate** | Get to the screen under test             | `Tap`, `Scroll`, `Wait`                              |
| **3 · Act**      | The scenario itself                      | `Tap`, `Type`, `MAP_ACTION`                          |
| **4 · Validate** | Assert the outcome                       | `Validate`                                           |
| **5 · Clean up** | Leave the device ready for the next test | `CLEAR_APP com.shopease.android`                     |

One scenario per test.

## Writing the lines

<table><thead><tr><th>Rule</th><th>Detail</th></tr></thead><tbody><tr><td><strong>Plain English, in the app's own words</strong></td><td>Use the exact wording on screen. <code>Tap on Charge Now</code></td></tr><tr><td>Continuous mode</td><td>Drag and select the lines if you only plan to run those over particular screens. <br>Note: Continuous mode works for the current state of UI</td></tr><tr><td><strong>The <code>/</code> palette is the authoritative list</strong></td><td>It shows every supported command, with autocomplete for package names, modules and variables. The same list is on the <a href="/pages/tOwn3l4aEfybJuOcrp7J">Command index</a></td></tr><tr><td><strong>Repeated setup belongs in a module</strong></td><td>Login, choosing a city, setting an address — write it once and <code>CALL</code> it. See <a href="/pages/92D9ohPqc6eyuPm4vkQJ">Modules</a></td></tr><tr><td><strong>Popups that can appear anywhere belong in Memory</strong></td><td>Not in every test. See <a href="/pages/wl2GqBAWrbDmrlNZGHGT">Memory &#x26; blockers</a></td></tr><tr><td><p><strong>Comments start with <code>#</code></strong><br><strong><code>Eg.</code></strong></p><pre><code>Tap on Proceed to Pay
# Tap on Apply Coupon
</code></pre></td><td>Use them to label sections and to park a step that shouldn't run yet</td></tr></tbody></table>

## Common mistakes

| What you wrote                                                    | What happens                                                                                                                                                  |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A test that covers login, search *and* checkout                   | One failure blocks three scenarios, and the report can't tell you which one is actually broken                                                                |
| No `CLEAR_APP` at the end                                         | The next test starts on whatever screen this one left behind, and passes or fails for unrelated reasons                                                       |
| `Tap on the payment button` when the button reads `Charge Now`    | Vision AI has to guess which control you meant. Wrong element, or a step that fails intermittently. Use double quotes to perform actions on spefic UI element |
| A command copied from an old script that isn't in the `/` palette | The line is classified as something else, or fails at run time. The palette is the source of truth                                                            |
| The same login steps pasted into 30 tests                         | A UI change to login breaks 30 tests instead of one module                                                                                                    |
| An `IF` block for a promo popup, repeated in every test           | The popup still interrupts the tests you forgot. Put it in Memory once                                                                                        |

## Next

* [Command index](/writing-tests/command-index) — every command, one table
* [Tap](/writing-tests/tap) — the most-used command
* [Waits & timing](/writing-tests/waits-and-timing) — fixed waits and where they go

***

*Last updated: 6 August 2026*


# Command index

Every command Drizz understands, in one table — syntax, what it does, and where the detail lives.

Every command Drizz understands, in one table. A command that isn't here isn't a command — press `/` in the editor to confirm.

## Prerequisites

* A connected device or emulator
* An open test file

## Commands

<table data-search="false"><thead><tr><th>Command</th><th>What it does</th><th>Page</th></tr></thead><tbody><tr><td><code># &#x3C;text></code></td><td>A comment. The line is skipped</td><td><a href="/pages/gtld3Y4Opi5xwkqoem4Z">The shape of a test</a></td></tr><tr><td><code>Tap on &#x3C;description></code></td><td>Tap an element described in plain English</td><td><a href="/pages/d27nnpfmF71JEaTqASja">Tap</a></td></tr><tr><td><code>Type &#x3C;text> in &#x3C;field></code></td><td>Enter text into a field, focusing it for you</td><td><a href="/pages/fmEafMB0dQcmof1UPSdo">Type</a></td></tr><tr><td><code>Validate &#x3C;condition></code></td><td>Assert something is true on screen</td><td><a href="/pages/iVT5httbNhMFAZxSuOuq">Validate</a></td></tr><tr><td><code>Scroll &#x3C;direction></code></td><td>Scroll by a fixed amount </td><td><a href="/pages/jLN1iZOjukUTycp1izVt">Scroll</a></td></tr><tr><td><code>Scroll &#x3C;direction> by &#x3C;n>%</code></td><td>Scroll by a specific amount</td><td><a href="/pages/jLN1iZOjukUTycp1izVt">Scroll</a></td></tr><tr><td><code>Scroll &#x3C;direction> until "&#x3C;target>" is visible</code></td><td>Scroll until something comes into view</td><td><a href="/pages/jLN1iZOjukUTycp1izVt">Scroll</a></td></tr><tr><td><code>Swipe</code> / <code>Drag</code> / <code>Slide</code></td><td>Synonyms of <code>Scroll</code> — same directional behavior</td><td><a href="/pages/jLN1iZOjukUTycp1izVt">Scroll</a></td></tr><tr><td><code>Wait Until &#x3C;n> Seconds</code></td><td>Pause for a fixed time</td><td><a href="/pages/ZvXqPjs8xSPAZoh7A12r">Waits &#x26; timing</a></td></tr><tr><td><code>IF &#x3C;condition> { … }</code></td><td>Run steps only when the condition holds</td><td><a href="/pages/Oeivxm4efPdtz01deJCa">Conditionals</a></td></tr><tr><td><code>ELSE IF &#x3C;condition> { … }</code></td><td>Next branch when the <code>IF</code> doesn't match</td><td><a href="/pages/Oeivxm4efPdtz01deJCa">Conditionals</a></td></tr><tr><td><code>ELSE { … }</code></td><td>Fallback branch</td><td><a href="/pages/Oeivxm4efPdtz01deJCa">Conditionals</a></td></tr><tr><td><code>MAP_ACTION Tap on &#x3C;description></code></td><td>Coordinate-based tap for unlabeled or canvas elements</td><td><a href="/pages/1Glmb1RT149RLaMImgFg">MAP_ACTION</a></td></tr><tr><td><code>MAP_ACTION Drag &#x3C;from> to &#x3C;to></code></td><td>Drag a slider, seekbar, map pin or swipe-to-confirm control</td><td><a href="/pages/1Glmb1RT149RLaMImgFg">MAP_ACTION</a></td></tr><tr><td><code>OPEN_APP &#x3C;package></code></td><td>Launch an app</td><td><a href="/pages/zKdcFjpjI2RcnV1dKT5U">System commands</a></td></tr><tr><td><code>KILL_APP &#x3C;package></code></td><td>Force-stop an app</td><td><a href="/pages/zKdcFjpjI2RcnV1dKT5U">System commands</a></td></tr><tr><td><code>CLEAR_APP &#x3C;package></code></td><td>Wipe app data and reset to first launch</td><td><a href="/pages/zKdcFjpjI2RcnV1dKT5U">System commands</a></td></tr><tr><td><code>MINIMISE_APP &#x3C;package></code></td><td>Send the app to the background</td><td><a href="/pages/zKdcFjpjI2RcnV1dKT5U">System commands</a></td></tr><tr><td><code>PRESS_DEVICE_BACK_BUTTON</code></td><td>Android hardware back</td><td><a href="/pages/zKdcFjpjI2RcnV1dKT5U">System commands</a></td></tr><tr><td><code>SET_GPS(latitude=&#x3C;lat>, longitude=&#x3C;lon>)</code></td><td>Mock the device location</td><td><a href="/pages/zKdcFjpjI2RcnV1dKT5U">System commands</a></td></tr><tr><td><code>ENABLE_WIFI</code> / <code>DISABLE_WIFI</code></td><td>Turn wifi on or off</td><td><a href="/pages/zKdcFjpjI2RcnV1dKT5U">System commands</a></td></tr><tr><td><code>TOGGLE_LOCATION</code></td><td>Toggle location services</td><td><a href="/pages/zKdcFjpjI2RcnV1dKT5U">System commands</a></td></tr><tr><td><code>SET &#x3C;var> = "&#x3C;value>"</code></td><td>Assign a variable</td><td><a href="/pages/IH55nOeZirs6dxVNXT7B">Store &#x26; SET</a></td></tr><tr><td><code>SET &#x3C;var> = screen(&#x3C;description>)</code></td><td>Capture structured data off the screen</td><td><a href="/pages/IH55nOeZirs6dxVNXT7B">Store &#x26; SET</a></td></tr><tr><td><code>{{var}}</code></td><td>Reference a variable in any command</td><td><a href="/pages/ZFNpIL6ghiA6xGSY7MqS">Which variable do I use?</a></td></tr><tr><td><code>{{var.field}}</code></td><td>Reference one field of a structured variable</td><td><a href="/pages/ZFNpIL6ghiA6xGSY7MqS">Which variable do I use?</a></td></tr><tr><td><code>CALL &#x3C;module></code></td><td>Run a reusable module</td><td><a href="/pages/92D9ohPqc6eyuPm4vkQJ">Modules</a></td></tr><tr><td><code>CALL &#x3C;module>(&#x3C;param>={{value}})</code></td><td>Run a module with parameters</td><td><a href="/pages/92D9ohPqc6eyuPm4vkQJ">Modules</a></td></tr><tr><td><code>PARAM &#x3C;name></code></td><td>Declare a module parameter, inside the module</td><td><a href="/pages/92D9ohPqc6eyuPm4vkQJ">Modules</a></td></tr><tr><td><code>API:&#x3C;api_name></code></td><td>Call a registered API</td><td><a href="/pages/jmLw5Bvdq3wPMMWtRzkY">API steps</a></td></tr></tbody></table>

## Syntax conventions

| Convention                                  | Detail                                                           |
| ------------------------------------------- | ---------------------------------------------------------------- |
| **Angle brackets are placeholders**         | `<package>` means you type `com.shopease.android`                |
| **Keywords are case-insensitive**           | `Validate`, `validate` and `VALIDATE` all work                   |
| **System commands are written in capitals** | `OPEN_APP`, not `open_app`                                       |
| **Validation keywords**                     | `Validate`, `Verify`, `Confirm` and `Check` are the same command |
| **Scroll conjunctions**                     | `until`, `till` and `untill` are all accepted after `Scroll`     |
| **Variables**                               | Anything in `{{ }}` is substituted before the step runs          |

## Common mistakes

| What you wrote                    | What happens                                                                                               |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `OPEN_APP` with no package name   | Fails by design. The package argument is required                                                          |
| `OPEN_APP:(com.shopease.android)` | Not supported. Use a space: `OPEN_APP com.shopease.android`                                                |
| `open_app com.shopease.android`   | System commands are written in capitals — write `OPEN_APP`                                                 |
| `Wait until the cart loads`       | Accepted by the editor, but only fixed-duration waits are confirmed to wait. Write `Wait Until 10 Seconds` |

## Next

* [The shape of a test](/writing-tests/writing-tests) — how the commands fit together
* [Tap](/writing-tests/tap) — the most-used command
* [Waits & timing](/writing-tests/waits-and-timing) — fixed waits and where they go

***

*Last updated: 6 August 2026*


# Actions

Tap an element by describing it in plain English, and the six ways to describe it so only one element matches.

`Tap` presses one element on screen. The description identifies the element; Vision AI locates it.

|                   |                                                                   |
| ----------------- | ----------------------------------------------------------------- |
| **Platforms**     | Android · iOS                                                     |
| **Targets by**    | Visible text, icon, position, neighboring element, section, color |
| **Best target**   | The element's exact visible label                                 |
| **Also accepted** | `Tap`, `Tap on`, `Tap the` — all the same command                 |
| **Watch out**     | Paraphrasing the label is the most common cause of a wrong tap    |

## Prerequisites

* A connected device or emulator
* An open test file
* The target screen reachable by the steps above the tap

## Copy this

```
OPEN_APP com.shopease.android
Wait Until 5 Seconds

Tap on the search icon
Type running shoes in the search field
Tap on the first search result
Wait Until 3 Seconds

Validate that the product detail page is visible
Tap on Add to Cart
Wait Until 2 Seconds
Validate that Item added to cart is visible
```

## Write a tap step

1. Read the element's exact visible label off the screen.
2. Write `Tap on <label>` using that wording, character for character.
3. Add one qualifier — position, neighbor, section, or color — when the label appears more than once on the screen.
4. Add `Wait Until <n> Seconds` after the tap when it triggers navigation or a network call.
5. Add a `Validate` on the next line when the tap navigates, submits or pays.
6. Run the test and confirm the report shows the tap resolving to the intended element.

## Targeting strategies

Drizz selects one element out of everything on screen. The description must be satisfied by exactly one element. The six strategies, in order of reliability:

| Strategy                        | Use when                                       | Example                                                         |
| ------------------------------- | ---------------------------------------------- | --------------------------------------------------------------- |
| **Exact visible label**         | The element has text                           | `Tap on Login CTA`                                              |
| **Icon by common name**         | The element has no text                        | `Tap the cart icon`                                             |
| **Position in a list**          | The same label repeats down the screen         | `Tap on the first Add beside product name`                      |
| **Relative to a neighbor**      | Each row has its own copy of the label         | `Tap Remove beside Product Details`                             |
| **Within a section**            | The same label appears in two sections         | `Tap Add under Snacks header`                                   |
| **Visual attribute or ranking** | State or a comparison is what distinguishes it | `Tap the green Active button` · `Tap the highest-rated product` |

## Examples

**Exact text — the default:**

```
Tap on Login CTA
Tap on Continue Button
Tap on Continue to Payment
```

**Icons, by the name people use for them:**

```
Tap the cart icon
Tap the profile icon
Tap the hamburger icon
Tap the settings icon
Tap the close icon
Tap the back arrow
Tap the search icon
```

**Position, when the label repeats:**

```
Tap on the first Add beside product name
Tap on the last Remove CTA
Tap the cart icon at top right
Tap Buy on the first card
```

**Section and neighbor context:**

```
Tap Add under Snacks header
Tap Remove beside Product Details
Tap Select next to Basic Plan option
Tap on % option under Discount
```

**Color and state, when nothing else separates them:**

```
Tap the green Active button
Tap the red Retry indicator
Tap the highlighted Selected tab
Tap the orange Offer badge
```

**Ranking and comparison — Drizz selects by value, not only by text:**

```
Tap the highest-rated product
Tap the lowest-priced item
Tap the plan with the maximum discount
Tap the restaurant with the most reviews
Tap the item with rating above 4.5
```

## Use the app's exact wording

If the button reads `Charge Now`, write `Charge Now` — not "Proceed to Pay", not "the payment button". Paraphrasing is the most common cause of a tap resolving to the wrong element, and it fails intermittently rather than immediately.

## Validate after a tap

A tap that navigates, submits or pays is followed by a `Validate`. Without one, a mis-resolved tap surfaces several steps later, on an unrelated screen.

```
Tap on Place Order
Wait Until 3 Seconds
Validate that Order Placed is visible
```

## When a tap won't resolve

Add context first — a neighbor, a section header, a position. [`MAP_ACTION`](/writing-tests/tap/map-action) targets elements that have no label and no clean bounding box.

{% hint style="success" %}
A tap that keeps failing immediately after a `Type`  might be a keyboard problem — see [Type](/writing-tests/tap/type).
{% endhint %}

## Common mistakes

<table data-search="false"><thead><tr><th>What you wrote</th><th>What happens</th></tr></thead><tbody><tr><td><code>Tap on the payment button</code> when the button reads <code>Charge Now</code></td><td>Vision AI guesses. It resolves to a different control, or fails on some runs and not others</td></tr><tr><td><code>Tap on Add</code> on a screen with five <code>Add</code> buttons</td><td>One of them is tapped, and which one can change between runs</td></tr><tr><td><code>Tap on the third product</code> when list order is server-driven</td><td>Passes on your device, fails on the next run with different data. Target by label instead</td></tr><tr><td><code>Tap on the blue button in the middle of the screen near the bottom</code></td><td>Long positional prose is weaker than the label. Use the label, add one qualifier</td></tr><tr><td><code>MAP_ACTION Tap on Continue</code> when <code>Continue</code> has a visible label</td><td>A deterministic tap traded for a grid guess</td></tr><tr><td>A tap immediately after <code>Type</code>, with no wait</td><td>The keyboard or a transition can still cover the target and the step reports as undoable</td></tr></tbody></table>

## Next

* [Type](/writing-tests/tap/type) — entering text, and keyboard handling
* [Validate](/writing-tests/tap/validate) — asserting the result of a tap
* [MAP\_ACTION](/writing-tests/tap/map-action) — unlabeled elements and gestures

***

*Last updated: 6 August 2026*


# Type

Enter text into a field. Drizz focuses the field, clears what's there, and dismisses the keyboard afterwards.

`Type` enters text into a field. The field does not need a separate tap first, and it does not need clearing.

|                   |                                                            |
| ----------------- | ---------------------------------------------------------- |
| **Platforms**     | Android · iOS                                              |
| **Focus**         | Automatic — no separate tap needed                         |
| **Existing text** | Replaced, not appended                                     |
| **Keyboard**      | Dismissed automatically after typing                       |
| **Watch out**     | Without a field name, the text can land in the wrong input |

## Prerequisites

* A connected device or emulator
* An open test file
* The screen containing the field reachable by the steps above the `Type`

## Copy this

```
OPEN_APP com.shopease.android
Wait Until 5 Seconds

Tap on Log in
Type qa@example.com in the email field
Type 9000000000 in the mobile number field
Tap on Continue
Wait Until 3 Seconds

Type 123456 in the OTP field
Tap on Verify
Wait Until 5 Seconds
Validate that the home screen is visible
```

## Write a type step

1. Confirm the screen has finished loading — add a `Wait` or a `Validate` above the `Type`.
2. Write `Type <value> in <field name>`, using the field's visible label.
3. Add a section, a position, or an adjacent value when the label appears more than once.
4. Add `Wait Until <n> Seconds` before the next tap when that tap targets an element the keyboard can cover.
5. Run the test and confirm the report shows the value in the intended field.

## Always name the field

The field name is what stops the text landing in the wrong input. On any screen with more than one field, name it.

```
Type qa@example.com in username field
Type password123 in password field
Type John into the name field
Type 9000000000 into the phone number field
```

| Field has                        | Write                                                                             |
| -------------------------------- | --------------------------------------------------------------------------------- |
| A unique visible label           | `Type John into the name field`                                                   |
| A label repeated in two sections | `Type John in personal details section` · `Type John in shipping details section` |
| No label                         | `Type 123 in the first input field` · `Type 456 in the second input field`        |
| An adjacent value only           | `Type 42 next to "Age"` · `Type 500 near "Quantity"`                              |

## Describing a value instead of supplying one

For fields that must be unique per run:

```
Type any random 10 digit number starting with 48 in Contact Number field
Type a random 8 character alphanumeric string in Reference Number field
```

## Variables

Any variable works inside a `Type`:

```
Type {{phone}} in the mobile number field
Type {{otp}} in the OTP field
```

See [Which variable do I use?](/writing-tests/which-variable).

## Automatic behavior

| Behavior               | Detail                                                                                                                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Focus**              | Drizz focuses the field. A separate tap first is allowed and helps on crowded screens, but is not required                                                                            |
| **Existing text**      | Replaced in full. Auto-populated and previously entered values are overwritten. Drizz does not append and does not edit inline — read the value with `Store` first to keep part of it |
| **Keyboard submit**    | `Tap on the search icon on the keyboard` works as a normal tap                                                                                                                        |
| **Keyboard dismissal** | On by default. An open keyboard covers the next element, and the tap that follows reports as undoable                                                                                 |

Keyboard dismissal is disabled for a small number of apps where dismissing broke the flow. The setting is not visible or editable in the desktop app.

{% hint style="warning" %}
If a tap immediately after a `Type` keeps failing as undoable, the keyboard is the first thing to suspect. Ask your Drizz contact whether keyboard handling is enabled for your app.
{% endhint %}

## OTP fields

| OTP input                   | Commands                                                                         |
| --------------------------- | -------------------------------------------------------------------------------- |
| One consolidated box        | `Type 123456 in the OTP field`                                                   |
| Split boxes, one digit each | <p>One command per box<br>Eg.<br>Type 1 in first box<br>Type 2 in second box</p> |

## Common mistakes

<table data-search="false"><thead><tr><th>What you wrote</th><th>What happens</th></tr></thead><tbody><tr><td><code>Type qa@example.com</code> with no field named</td><td>Focus lands wherever the app left it. On a two-field screen this is a coin flip</td></tr><tr><td><code>Type John in the name field</code> on a form with <code>First name</code> and <code>Last name</code></td><td>Ambiguous. Name the field exactly, or add the section</td></tr><tr><td>A <code>Type</code> immediately after the screen appears</td><td>Typing during a transition or while the field is still loading drops characters. Add a <code>Wait</code> or a <code>Validate</code> first</td></tr><tr><td><code>Tap on the field</code> then <code>Type</code> then <code>Tap on Submit</code>, with no wait</td><td>The extra tap is harmless, but the keyboard may still be closing when the <code>Submit</code> tap fires</td></tr><tr><td>Expecting <code>Type 5</code> to append to an existing <code>10</code></td><td>You get <code>5</code>. Existing text is replaced. <code>Store</code> the value first if you need to build on it</td></tr><tr><td><code>Type 123456 in the OTP field</code> on a six-box OTP input</td><td>Only the first box receives input. Write one command per box</td></tr><tr><td>Typing a value the app rejects, with no <code>Validate</code> after</td><td>The run continues on an error screen and fails somewhere unrelated</td></tr></tbody></table>

## Next

* [Tap](/writing-tests/tap) — the command that follows a `Type`
* [Validate](/writing-tests/tap/validate) — check what you typed landed
* [Which variable do I use?](/writing-tests/which-variable) — typing values from a dataset

***

*Last updated: 6 August 2026*


# Validate

Assert that something is true on screen — presence, state, numbers, or a value you captured earlier.

`Validate` asserts a condition on screen. A test passes or fails on its validations; every other command exists to reach the screen.

|               |                                                                               |
| ------------- | ----------------------------------------------------------------------------- |
| **Platforms** | Android · iOS                                                                 |
| **Keywords**  | `Validate`, `Verify`, `Confirm`, `Check` — any case                           |
| **Retries**   | Up to 3 attempts before the step fails                                        |
| **Batching**  | Several checks on one line are evaluated together, in one pass                |
| **Watch out** | Retries are not a wait. A screen that reliably takes 4 seconds needs a `Wait` |

## Prerequisites

* A connected device or emulator
* An open test file
* The screen under assertion reached by the steps above the `Validate`
* For variable comparisons: a value captured with `SET`, or an API step

## Copy this

```
OPEN_APP com.shopease.android
Wait Until 5 Seconds

Tap on the cart icon
Wait Until 3 Seconds

Validate 1. Order Summary 2. Delivery address 3. Total amount is visible
Validate that the Place Order button is enabled
Validate that the error message is not visible
```

## Write a validation

1. Add a `Wait` after the step that navigates to the screen.
2. Write `Validate <full visible label> is visible`, using the label exactly as it renders.
3. Add a section, a position, or a neighbor when the label appears more than once on the screen.
4. Number the checks on one line when several apply to the same screen.
5. Run the test and confirm the report lists each sub-check with its own result.

## Assertion forms

<table data-search="false"><thead><tr><th>Form</th><th>Syntax</th><th>Example</th></tr></thead><tbody><tr><td><strong>Presence</strong></td><td><code>Validate &#x3C;label> is visible</code></td><td><code>Validate Order Placed tag is visible</code></td></tr><tr><td><strong>Absence</strong></td><td><code>Validate &#x3C;label> is not visible</code></td><td><code>Validate that the error message is not visible</code></td></tr><tr><td><strong>State</strong></td><td><code>Validate &#x3C;label> is enabled</code> / <code>is disabled</code></td><td><code>Validate that the Place Order button is enabled</code></td></tr><tr><td><strong>Styling</strong></td><td><code>Validate &#x3C;label> is &#x3C;state> with &#x3C;style></code></td><td><code>Validate that the Submit button is disabled with grey colour</code></td></tr><tr><td><strong>Position</strong></td><td><code>Validate the &#x3C;ordinal> "&#x3C;label>"</code></td><td><code>Validate the first "Add" CTA</code></td></tr><tr><td><strong>Context</strong></td><td><code>Validate &#x3C;label> in &#x3C;section></code></td><td><code>Validate Active tag is visible in subscription section</code></td></tr><tr><td><strong>Comparison</strong></td><td><code>Validate &#x3C;a> is greater than &#x3C;b></code></td><td><code>Validate that the item count is greater than 0</code></td></tr><tr><td><strong>Arithmetic</strong></td><td><code>Validate &#x3C;a> equals &#x3C;expression></code></td><td><code>Validate that final amount equals subtotal + tax + delivery charges</code></td></tr><tr><td><strong>Against a variable</strong></td><td><code>Validate &#x3C;thing on screen> matches {{var}}</code></td><td><code>Validate that the total shown on screen matches {{order.total_amount}}</code></td></tr><tr><td><strong>Partial text</strong></td><td><code>Validate text starts with "&#x3C;text>"</code> / <code>contains "&#x3C;text>"</code></td><td><code>Validate text contains "Success"</code></td></tr><tr><td><strong>Batched</strong></td><td><code>Validate 1. &#x3C;a> 2. &#x3C;b> 3. &#x3C;c> is visible</code></td><td><code>Validate 1. Order ID 2. Delivery address 3. Total amount is visible</code></td></tr></tbody></table>

## Presence

Use the full visible label.

```
Validate Home is visible
Validate Order Placed tag is visible
Validate Login CTA is visible
Validate that the Customer Name is displayed
Validate that the error message is not visible
```

Add context when the label appears more than once:

```
Validate Active tag is visible in subscription section
Validate Apply CTA is enabled in the coupon area
Validate Address is present under Profile
Validate In Stock tag near product title
Validate ₹499 next to MRP is present
```

Add position when identical labels repeat:

```
Validate the first "Add" CTA
Validate the last "Remove" CTA
Validate "Delivered" tag is visible in order history list
```

## Several things at once

Number the checks on one line. Drizz evaluates checks on the same screen in a single pass, which costs less than three separate validations.

```
Validate 1. Order ID 2. Delivery address 3. Total amount is visible
Validate 1. Booking confirmed header 2. Pickup and drop location is visible
Validate 1. Book an ambulance is visible 2. Bottom confirmation sheet is not visible
```

{% hint style="warning" %}
If a batched validation reports false while every sub-check reads true, split it across two lines.
{% endhint %}

## Styling and state

```
Validate that the Submit button is disabled with grey colour
Validate that the selected tab is highlighted with a white outline around it
Validate cancel CTA is disabled beside Apply coupon of first coupon
Validate "Select" is enabled in delivery options
```

## Numbers and comparisons

Drizz evaluates arithmetic rather than reading the total off the screen.

```
Validate that the item count is greater than 0
Validate that total equals sum of item prices
Validate that discount equals 10% of subtotal
Validate that final amount equals subtotal + tax + delivery charges
Validate that payable amount equals total - discount
Validate that line item amount equals unit price multiplied by quantity
```

Group calculations that belong together onto one line so they're evaluated as a set:

```
Validate the following calculations: 1. Subtotal+Tax=Total 2. Total-Discount=Final Amount
```

Directional checks (`greater than`) survive a pricing change. A hard-coded expected total does not.

## Against a variable or an API response

Compare what's on screen to a value captured earlier or pulled from an API.

```
Validate that the total shown on screen matches {{order.total_amount}}
Validate {{memberDetails.fullname}} is visible on the screen
Validate vehicle name shown on screen matches selected_vehicle_api.make_model
Validate 1. File a claim is visible on screen 2. selected_vehicle_api.policy_count is greater than 0
```

See [Store & SET](/writing-tests/which-variable/store-and-set) for capturing the values, and [API steps](/writing-tests/api-steps) for pulling them from an API.

## Inside a conditional

```
IF While using the app CTA is visible
{
    Tap on While using the app CTA
}
```

## Dynamic text

When only part of the label is stable, validate the stable part.

```
Validate text starts with "Order"
Validate text contains "Success"
```

Full-text validation applies everywhere else. Partial matches over-match.

## Retries

A validation gets up to **3 attempts** before it fails. Retries absorb jitter, not latency. A screen that reliably takes four seconds needs `Wait Until 4 Seconds` — by the time three attempts are exhausted, the step has failed on a screen that was never ready. See [Waits & timing](/writing-tests/waits-and-timing).

## Redundant validations

A `Scroll ... until "FAQs" is visible` that succeeded has already put the element on screen. A `Validate FAQs is visible` on the next line is a duplicate check that costs a pass over the screen.

## Common mistakes

<table data-search="false"><thead><tr><th>What you wrote</th><th>What happens</th></tr></thead><tbody><tr><td><code>Validate Placed is visible</code> when the label reads <code>Order Placed</code></td><td>A partial match can hit the wrong element. Validate the full label</td></tr><tr><td><code>Validate Apply is visible</code> on a screen with three coupons</td><td>Matches whichever one it finds first. Add the section or a position</td></tr><tr><td>A <code>Validate</code> immediately after a tap that navigates</td><td>The validation runs against the old screen, burns its 3 attempts and fails. Add a <code>Wait</code></td></tr><tr><td><code>Validate that total is 1499</code> with server-driven pricing</td><td>Passes today, fails the next time pricing changes. Use <code>greater than</code> or compare against a stored value</td></tr><tr><td>Validating a variable's <em>name</em> — <code>Validate phone is visible</code></td><td>Checks for the literal text "phone". Validate the value, or the label it sits under</td></tr><tr><td>Three separate <code>Validate</code> lines for one screen</td><td>Three passes over the same screen. Number them on one line instead</td></tr><tr><td>A test that taps through five screens and validates nothing</td><td>Passes even when the app is broken</td></tr></tbody></table>

## Next

* [Waits & timing](/writing-tests/waits-and-timing) — the fix for validations that fail on slow screens
* [Conditionals](/writing-tests/conditionals) — `IF Validate` blocks
* [Store & SET](/writing-tests/which-variable/store-and-set) — capturing values to validate against

***

*Last updated: 6 August 2026*


# Scroll

Scroll by a fixed amount, or scroll until a target comes into view. Quote the target, give a direction, validate afterwards.

`Scroll` moves the viewport, either by a fixed amount or until a named target comes into view.

|                  |                                                                                                                                                       |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Platforms**    | Android · iOS                                                                                                                                         |
| **Default step** | 50% of the screen                                                                                                                                     |
| **Max scrolls**  | 20 (configurable per app)                                                                                                                             |
| **Direction**    | Up, down, left, right — the direction your thumb moves                                                                                                |
| **Watch out**    | <p>Scoll direction is determined by human interpretation of thumb movement on UI<br>Eg. To scroll page upwards direction would be "up" not "down"</p> |

## Prerequisites

* A connected device or emulator
* An open test file
* A screen that has finished loading before the scroll runs

## Copy this

```
OPEN_APP com.shopease.android
Wait Until 5 Seconds

Tap on the cart icon
Wait Until 3 Seconds

Scroll down until "Proceed to Pay" is visible
Validate that Proceed to Pay is visible
Tap on Proceed to Pay
```

## Write a scroll step

1. Confirm the screen has finished loading — add a `Wait` or a `Validate` above the scroll.
2. Write `Scroll <direction> until "<target>" is visible`, with the target in quotes.
3. Add `inside <container>` when the screen has more than one scrollable region.
4. Add a step size or a max-scroll override when the default 50% step or 20-scroll limit doesn't reach the target.
5. Run the test and confirm the report shows the scroll resolving before the validation.

## Parameters

| Parameter        | Syntax                                                                                          | Default                             | Notes                                                                                         |
| ---------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------- |
| **Direction**    | `Scroll up` · `down` · `left` · `right`                                                         | None — always state it              | The direction your thumb moves, not the direction the content moves                           |
| **Target**       | `until "<target>" is visible`                                                                   | None                                | Quote the target. Quotes give Drizz an exact string to match instead of a phrase to interpret |
| **Step size**    | `by <n>%` · `in steps of <n>%` · `with scroll percentage of <n>%` · `Scroll every time by <n>%` | 50% of the screen                   | Drops to 35% automatically when Drizz detects a scroll had no effect                          |
| **Max scrolls**  | `with max of <n> scrolls` · `(Max scroll - <n>)`                                                | 20                                  | Configurable per app — some apps run at 25, 30 or 40                                          |
| **Container**    | `inside <container>`                                                                            | The scrollable region Drizz selects | Required on screens with more than one scrollable region                                      |
| **Repeat count** | `Scroll thrice`                                                                                 | One scroll                          | Fixed scrolls only; scroll will only happen once                                              |

`until`, `till` and `untill` all work, and `Swipe` is interchangeable with `Scroll`.

To change the defaults for your app rather than per command, ask your Drizz contact. They're per-app settings.

## Scroll until a target is visible

```
Scroll down until "Proceed to Pay" is visible
Scroll up until "Home" tag is visible
Scroll right until "Next" CTA is visible
Scroll down until "Apply Coupon" is visible
Scroll up until "Back to Top" is visible
```

The target can be described rather than named:

```
Scroll down until you find 4 product cards under "Beauty"
Scroll down until items appear under "Snacks"
Scroll up until offers are visible under "Top Deals"
```

Name the container on screens with more than one scrollable region:

```
Scroll down inside product list until "Add to Cart" is visible
Scroll right inside categories until "Electronics" is visible
Scroll down inside reviews until "Write a Review" is visible
```

## Fixed scroll

Not ideal way of using the command as it could toss the reliability.

`Scroll until` stops when it finds the target. A fixed scroll moves the set distance whether or not the target is on screen.

## Tuning the step and the limit

Both can be set on the command:

```
Scroll up until "Choose a customer type" is visible in steps of 30%
Scroll down until "Order Summary" is visible with max of 40 scrolls
Scroll down inside product list with scroll percentage of 30%
Scroll Up until you find "Other" (Max scroll - 50, Scroll every time by 100%)
```

## Hitting the limit fails the step

A scroll that runs out its max-scroll count without finding the target **stops without failing**. The test stops and the next actionable command won't run reliably.

## One target per command

Bringing two things into view takes two scroll commands.\
Note: The target can have primary target and secondary target. Example- Scroll up until "Chocolates" is visible under "Kids" header.

```
Scroll down until "Order Summary" is visible
Scroll down until "Proceed to Pay" is visible
```

## Self-healing

When a scroll is blocked by a sticky header, an overlapping element or a nested scroll view, Drizz detects that the screen didn't move and retries with a reduced step. The report shows the reduced scroll distance. It is automatic. It also indicates that the container being scrolled is not the intended one.

## Common mistakes

<table data-search="false"><thead><tr><th>What you wrote</th><th>What happens</th></tr></thead><tbody><tr><td><code>Scroll until Proceed to Pay</code> — no quotes, no direction</td><td>Drizz has to infer both. Unstable across runs and devices</td></tr><tr><td><code>Scroll down</code> when you meant to reveal content above</td><td>Down moves your thumb down, which reveals content <em>above</em>. Use <code>Scroll up</code></td></tr><tr><td><code>Scroll down until "Checkout" is visible</code> with nothing after it</td><td>If the target is never found, the step stops silently and the next command runs on the wrong screen</td></tr><tr><td><code>Scroll down until "Total" and "Proceed" are visible</code></td><td>One target per command. Write two lines</td></tr><tr><td>A scroll on a screen with a horizontal carousel and a vertical list</td><td>The wrong region moves. Name the container: <code>Scroll down inside product list</code></td></tr><tr><td><code>Scroll down until "Item 200" is visible</code> in a 300-item list</td><td>Stops at 20 scrolls by default. Raise it on the command, or ask for a per-app default</td></tr><tr><td>A scroll issued while the screen is still loading</td><td>Nothing scrolls, or the wrong container does. Wait for the screen first</td></tr><tr><td>A script written against the old scroll-back behavior</td><td>Since August 2026 the viewport stays where the last target was found. Split it into two scroll commands</td></tr></tbody></table>

## Next

* [Validate](/writing-tests/tap/validate) — what to put after a scroll
* [Waits & timing](/writing-tests/waits-and-timing) — scrolling a screen that hasn't settled
* [Command index](/writing-tests/command-index) — every command in one table

***

*Last updated: 6 August 2026*


# MAP\_ACTION

Coordinate-based taps, drags and pinches for map surfaces, canvases and sliders — the least deterministic command in Drizz.

`MAP_ACTION` acts on the visible surface rather than on a labeled element. It covers maps, canvases and sliders, and applies when normal targeting has failed.

|                  |                                                          |
| ---------------- | -------------------------------------------------------- |
| **Platforms**    | <p>Tap   :  Android · iOS <br>Drag :  Android · iOS </p> |
| **Pinch / zoom** | Specific Android emulators only. Not iOS                 |

## Prerequisites

* A connected device or emulator
* An open test file
* A target element with no visible label and no clean bounding box, or a gesture that isn't a tap
* The surface fully rendered before the step runs

## Copy this

```
OPEN_APP com.shopease.android
Wait Until 5 Seconds

Tap on Filters
Wait Until 2 Seconds

MAP_ACTION Drag the price slider from 200 to 800
Validate that the price range shows 800
Tap on Apply
```

## Write a MAP\_ACTION step

1. Confirm the element has no visible label. If it has one, use [`Tap`](/writing-tests/tap).
2. Add a `Wait` above the step so the surface is fully rendered.
3. Start the line with `MAP_ACTION`, then describe the gesture in plain English.
4. State both ends of a drag — where it starts and where it ends.
5. Add a `Validate` on the next line for the resulting state.
6. Run the test several times and confirm the same cell resolves on each run.

## Syntax

| Element       | Form                                           | Notes                                                                                                         |
| ------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Keyword**   | `MAP_ACTION`                                   | Space separator. `MAP_ACTION:` also works and appears in older scripts — write the space form in anything new |
| **Gesture**   | `Tap on <description>` · `Drag <from> to <to>` | The `/` palette lists two entries, `MAP_ACTION Tap` and `MAP_ACTION Drag`                                     |
| **Drag path** | `from <start> to <end>`                        | Both ends required                                                                                            |

```
MAP_ACTION Tap on the red location pin
MAP_ACTION Drag map pointer from left to right
```

## Tap

For map markers, canvas controls and boxes with no label of their own.

```
MAP_ACTION Tap on the red location pin
MAP_ACTION Tap on the map
MAP_ACTION Tap on the pickup marker near "Station"
MAP_ACTION Tap on the small white input box beside Rack / Conveyor 1 label
```

## Drag

For sliders, seekbars, time pickers, map panning and swipe-to-confirm controls. State where the drag starts and where it ends.

```
MAP_ACTION Drag map pointer from left to right
MAP_ACTION Drag from bottom to top
MAP_ACTION Drag from right to left to reposition marker
MAP_ACTION Drag from top to bottom to view surrounding region
MAP_ACTION Drag the price slider from 200 to 800
MAP_ACTION Drag the "hour picker" from 9 to 8
MAP_ACTION Drag up once on the left slider
MAP_ACTION Drag the green pin on the map from the store to the delivery address
```

Directional phrasing that works: *from left to right*, *from bottom to top*, *from top right to center of screen*.

## Pinch and zoom

```
MAP_ACTION pinch to zoom in twice
MAP_ACTION Perform a "pinch-to-zoom" gesture on the map
MAP_ACTION pinch to zoom in on object
```

{% hint style="warning" %}
Pinch and zoom are implemented for specific Android emulators only. They do not work on iOS, and they are not available on every device target. Tap and Drag work everywhere.
{% endhint %}

## Platform support

{% tabs %}
{% tab title="Android" %}

| Gesture      | Support                 | Notes                                           |
| ------------ | ----------------------- | ----------------------------------------------- |
| `Tap`        | ✅                       | Dispatches the same primitive as a normal tap   |
| `Drag`       | ✅                       | Dispatches the same primitive as a normal swipe |
| Pinch / zoom | Specific emulators only | Not available on every Android device target    |
| {% endtab %} |                         |                                                 |

{% tab title="iOS" %}

| Gesture       | Support | Notes                                                                    |
| ------------- | ------- | ------------------------------------------------------------------------ |
| `Tap`         | ✅       | Dispatches the same primitive as a normal tap                            |
| `Drag`        | ✅       | Dispatches the same primitive as a normal swipe                          |
| Pinch / zoom  | ❌       | Not implemented on iOS. Drop pinch from cross-platform tests, or gate it |
| {% endtab %}  |         |                                                                          |
| {% endtabs %} |         |                                                                          |

## Determinism

`MAP_ACTION` overlays a numbered grid on the screen and picks a cell. That is what lets it work where element targeting can't. Two runs of the same script can select different cells, most often when a tappable element straddles a grid boundary.

A `Validate` on the next line turns a wrong cell into a visible failure in the report instead of a failure three screens later.

```
MAP_ACTION Drag the price slider from 200 to 800
Validate that the price range shows 800
```

## Common mistakes

<table data-search="false"><thead><tr><th>What you wrote</th><th>What happens</th></tr></thead><tbody><tr><td><code>MAP_ACTION Tap on Continue</code> when <code>Continue</code> has a visible label</td><td>A deterministic tap replaced with a grid guess, for no benefit</td></tr><tr><td><code>MAP_ACTION Drag the slider</code> with no start and end</td><td>The drag has no defined path. State both ends</td></tr><tr><td><code>MAP_ACTION pinch to zoom in</code> in a suite that runs on iOS</td><td>The step doesn't run there. Gate it, or drop pinch from cross-platform tests</td></tr><tr><td>No <code>Validate</code> after a <code>MAP_ACTION</code></td><td>A wrong cell passes silently and the test fails somewhere unrelated</td></tr><tr><td><code>MAP_ACTION</code> on a normal list or form</td><td>Grid targeting on a screen that has a perfectly good element tree. Use <code>Tap</code> and <code>Scroll</code></td></tr><tr><td>A <code>MAP_ACTION</code> that worked once, trusted without a retry check</td><td>Repeat runs can pick a different cell. Run it several times before you rely on it</td></tr><tr><td><code>MAP_ACTION</code> issued while the map is still rendering</td><td>The grid is drawn over a half-loaded surface. Wait for the screen to settle first</td></tr></tbody></table>

## Next

* [Tap](/writing-tests/tap) — label-based targeting
* [Validate](/writing-tests/tap/validate) — what to put after a `MAP_ACTION`
* [Waits & timing](/writing-tests/waits-and-timing) — letting a map surface settle

***

*Last updated: 6 August 2026*


# System commands

The nine commands that control the app lifecycle and the device — OPEN\_APP, KILL\_APP, CLEAR\_APP and the rest, in one table.

System commands act on the app and the device rather than on anything on screen. They have fixed syntax and are written in capitals.

|                      |                                                                    |
| -------------------- | ------------------------------------------------------------------ |
| **Platforms**        | Android · iOS — with differences, see below                        |
| **Separator**        | A space. `OPEN_APP:com.shopease.android` is legacy but still works |
| **Package argument** | Required on `OPEN_APP`, `KILL_APP`, `CLEAR_APP`, `MINIMISE_APP`    |
| **Variables**        | `OPEN_APP {{app_package}}` works                                   |
| **Watch out**        | `CLEAR_APP` is effectively a no-op on iOS                          |

## Prerequisites

* A connected device or emulator
* An open test file
* A registered app and its package name — `com.shopease.android` in these examples

## Copy this

```
OPEN_APP com.shopease.android
Wait Until 5 Seconds

Tap on Log in
Type 9000000000 in the mobile number field
Tap on Continue
Wait Until 3 Seconds
Type 123456 in the OTP field
Tap on Verify
Wait Until 5 Seconds

Validate that the home screen is visible

CLEAR_APP com.shopease.android
```

## All nine commands

| Command                                    | What it does                                    | Argument                                  | Required  |
| ------------------------------------------ | ----------------------------------------------- | ----------------------------------------- | --------- |
| `OPEN_APP <package>`                       | Launches the app                                | Package name                              | Yes       |
| `KILL_APP <package>`                       | Force-stops the app and clears it from memory   | Package name                              | Yes       |
| `CLEAR_APP <package>`                      | Wipes app data and resets to first-launch state | Package name                              | Yes       |
| `MINIMISE_APP <package>`                   | Sends the app to the background                 | Package name                              | Yes       |
| `PRESS_DEVICE_BACK_BUTTON`                 | Presses the Android hardware back button        | None                                      | —         |
| `SET_GPS(latitude=<lat>, longitude=<lon>)` | Sets the device location                        | Named args in parentheses, latitude first | Yes, both |
| `ENABLE_WIFI`                              | Turns wifi on                                   | None                                      | —         |
| `DISABLE_WIFI`                             | Turns wifi off                                  | None                                      | —         |
| `TOGGLE_LOCATION`                          | Toggles location services                       | None                                      | —         |

```
OPEN_APP com.shopease.android
KILL_APP com.shopease.android
CLEAR_APP com.shopease.android
MINIMISE_APP com.shopease.android
PRESS_DEVICE_BACK_BUTTON
SET_GPS(latitude=12.9716, longitude=77.5946)
ENABLE_WIFI
DISABLE_WIFI
TOGGLE_LOCATION
```

## Syntax rules

<table data-search="false"><thead><tr><th>Rule</th><th>Detail</th></tr></thead><tbody><tr><td><strong>Space before the package name</strong></td><td><code>OPEN_APP com.shopease.android</code> is the current form</td></tr><tr><td><strong>Legacy separator</strong></td><td><code>OPEN_APP:com.shopease.android</code> (colon, no space) still works and appears in older scripts. It is tolerated, not recommended</td></tr><tr><td><strong>Unsupported form</strong></td><td><code>OPEN_APP:(com.shopease.android)</code> — colon plus parentheses — is not supported</td></tr><tr><td><strong>Package argument required</strong></td><td>On <code>OPEN_APP</code>, <code>KILL_APP</code>, <code>CLEAR_APP</code> and <code>MINIMISE_APP</code>. A bare <code>OPEN_APP</code>, or <code>KILL_APP :</code> with nothing after it, fails by design. The editor offers a package-name dropdown for these commands</td></tr><tr><td><strong>Spelling</strong></td><td><code>MINIMIZE_APP</code> (US spelling) and <code>MINIMISE_APP</code> are both accepted</td></tr><tr><td><strong><code>SET_GPS</code> arguments</strong></td><td>Named, in parentheses, latitude first. Use real coordinates — ETA, pricing, distance and availability all key off them</td></tr><tr><td><strong>One action per line</strong></td><td>System commands are not chained</td></tr></tbody></table>

```
SET_GPS(latitude=12.9716, longitude=77.5946)
```

## Using a variable for the package

For a script that runs against more than one build or environment:

```
SET app_package = "com.shopease.android"
OPEN_APP {{app_package}}
```

{% hint style="danger" %}
**Never pass a whole dataset to `OPEN_APP`.** `OPEN_APP {{my_dataset}}` serializes the entire row and the package name is parsed out of the wrong part of it — the app either fails to launch or launches something unexpected. Bind a single variable, not a dataset.
{% endhint %}

## Platform behavior

{% tabs %}
{% tab title="Android" %}

| Command                                  | Behavior                                               |
| ---------------------------------------- | ------------------------------------------------------ |
| `OPEN_APP` / `KILL_APP` / `MINIMISE_APP` | ✅ Supported                                            |
| `CLEAR_APP`                              | ✅ Real data wipe. The app resets to first-launch state |
| `PRESS_DEVICE_BACK_BUTTON`               | ✅ Presses the hardware back button                     |
| `SET_GPS`                                | ✅ Supported                                            |
| `ENABLE_WIFI` / `DISABLE_WIFI`           | ✅ Supported                                            |
| `TOGGLE_LOCATION`                        | ✅ Supported                                            |
| {% endtab %}                             |                                                        |

{% tab title="iOS" %}

| Command                                  | Behavior                                                                  |
| ---------------------------------------- | ------------------------------------------------------------------------- |
| `OPEN_APP` / `KILL_APP` / `MINIMISE_APP` | ✅ Supported                                                               |
| `CLEAR_APP`                              | ⚠️ Effectively does nothing - iOS doesn't support                         |
| `PRESS_DEVICE_BACK_BUTTON`               | ❌ Doesn't exist on iOS. Use `Tap on the back button` or a left-edge swipe |
| `SET_GPS`                                | ⚠️ Set, but not reliably readable back on every device target             |
| `ENABLE_WIFI` / `DISABLE_WIFI`           | ⚠️ Effectively does nothing - iOS doesn't support                         |
| `TOGGLE_LOCATION`                        | ⚠️ Effectively does nothing - iOS doesn't support                         |
| {% endtab %}                             |                                                                           |
| {% endtabs %}                            |                                                                           |

## Write a teardown

1. Decide the platforms the test runs on.
2. Add the teardown for those platforms as the last steps of the test.
3. Validate the state the teardown leaves behind.

{% tabs %}
{% tab title="Android" %}
`CLEAR_APP` wipes app data, so one line resets the app to first launch.

```
# Android teardown
CLEAR_APP com.shopease.android
```

{% endtab %}

{% tab title="iOS" %}
`CLEAR_APP` is a no-op on iOS: a script that relies on it for clean state passes on Android and reuses state on iOS. Reset inside the app, or install fresh between tests.

```
# Cross-platform teardown
Tap on the profile icon
Scroll down until "Log out" is visible
Tap on Log out
Validate that the login screen is visible
```

{% endtab %}
{% endtabs %}

## Ordering

`OPEN_APP` comes first. Comments, `CLEAR_APP`, `SET_GPS`, `PRESS_DEVICE_BACK_BUTTON`, API steps and waits are allowed before it, for setting location or clearing state before the app launches.

```
CLEAR_APP com.shopease.android
SET_GPS(latitude=12.9716, longitude=77.5946)
OPEN_APP com.shopease.android
Wait Until 5 Seconds
```

## Validate after them

System commands report nothing about the screen that follows. Follow the ones that change state with a validation.

```
KILL_APP com.shopease.android
OPEN_APP com.shopease.android
Wait Until 5 Seconds
Validate that the login screen is visible
```

## Common mistakes

<table data-search="false"><thead><tr><th>What you wrote</th><th>What happens</th></tr></thead><tbody><tr><td><code>OPEN_APP</code> with no package name</td><td>Fails by design. The argument is required</td></tr><tr><td><code>OPEN_APP:(com.shopease.android)</code></td><td>Not a supported form. Use a space</td></tr><tr><td><code>open_app com.shopease.android</code></td><td>System commands are written in capitals</td></tr><tr><td><code>OPEN_APP {{my_dataset}}</code></td><td>The whole row is serialized and the package name is parsed out of the wrong part of it</td></tr><tr><td><code>CLEAR_APP</code> as the clean-state step in an iOS suite</td><td>Passes, does nothing, and the next test starts logged in</td></tr><tr><td><code>PRESS_DEVICE_BACK_BUTTON</code> in a cross-platform script</td><td>Works on Android, doesn't exist on iOS. Use <code>Tap on the back button</code> or a left-edge swipe</td></tr><tr><td><code>TOGGLE_LOCATION</code> as a dependable precondition</td><td>Unreliable. Drive the setting through the app or a module instead</td></tr><tr><td><code>KILL_APP</code> and <code>OPEN_APP</code> on consecutive lines with no wait</td><td>The first tap after launch lands on a splash screen</td></tr><tr><td>Two system commands on one line</td><td>Only one is executed. One per line</td></tr></tbody></table>

## Next

* [Which variable do I use?](/writing-tests/which-variable) — binding a package name per environment
* [The shape of a test](/writing-tests/writing-tests) — where setup and teardown sit
* [Command index](/writing-tests/command-index) — every command in one table

***

*Last updated: 6 August 2026*


# Waits & timing

Drizz supports fixed-duration waits: where to put them, how to spell them, and why element-conditioned wording is not a substitute.

Drizz supports **fixed-duration waits**. A wait states how long to pause, not what to wait for.

|                      |                                                                                                                   |
| -------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **Platforms**        | Android · iOS                                                                                                     |
| **What's supported** | Fixed durations only                                                                                              |
| **Spellings**        | `Wait Until 3 Seconds` · `Wait for 5 seconds` · `Wait until 10s` · `Wait 5s`                                      |
| **Cost**             | Wait steps are not billed                                                                                         |
| **Watch out**        | Element-conditioned wording (`Wait until "Cart"`) is accepted by the editor but has no confirmed polling behavior |

## Prerequisites

* A connected device or emulator
* An open test file

## Copy this

```
OPEN_APP com.shopease.android
Wait Until 5 Seconds

Tap on Log in
Type 9000000000 in the mobile number field
Tap on Continue
Wait Until 3 Seconds

Type 123456 in the OTP field
Tap on Verify
Wait Until 5 Seconds

Validate that the home screen is visible
```

## Size a wait

1. Run the test and note the slowest time the screen took to appear.
2. Write `Wait Until <n> Seconds` with that duration, rounded up.
3. Put the wait directly after the step that triggers the navigation or network call, before the step that needs the new screen.
4. Add a `Validate` on the next line. Validations get up to 3 attempts, which absorbs jitter on top of the wait.
5. Re-run the test and confirm the validation passes on its first attempt.

```
Tap on Place Order
Wait Until 5 Seconds
Validate that Order Placed is visible
```

## The four spellings

All four are the same command:

| Spelling               | Notes                                     |
| ---------------------- | ----------------------------------------- |
| `Wait Until 3 Seconds` | The form the editor's `/` palette inserts |
| `Wait for 5 seconds`   | -                                         |
| `Wait until 10s`       | -                                         |
| `Wait 5s`              | -                                         |

Note: Use one spelling across a suite.

## Where waits go

| Position                                                        | Duration                              |
| --------------------------------------------------------------- | ------------------------------------- |
| After any tap that navigates or triggers a network call         | Sized to the slowest observed run     |
| After login, before the first validation on the landing screen  | Sized to the slowest observed run     |
| After a payment or a submission, before validating the result   | Sized to the slowest observed run     |
| After `OPEN_APP`                                                | 3 to 5 seconds. More for a cold start |
| Before a `Type` into a field that renders after the screen does | Sized to the field's render time      |

{% hint style="success" %}
Waits are not billed. A wait costs wall-clock time in the run and nothing else.
{% endhint %}

## Retries are not waits

|                        | What it does                                                             | What it doesn't do                                                |
| ---------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| **A wait**             | Pauses for a fixed duration                                              | Nothing else — it doesn't check the screen                        |
| **Validation retries** | Re-checks the assertion up to 3 times in case of transition state of app | Doesn't help a `Tap`, `Type` or `Scroll` land on the right screen |

Validation retries cover a screen that arrives a moment late. They do not cover a screen that takes four seconds — the three attempts are exhausted before the screen is ready, and the step fails.

## Common mistakes

<table data-search="false"><thead><tr><th>What you wrote</th><th>What happens</th></tr></thead><tbody><tr><td><code>Wait until "Proceed to Pay" is visible</code></td><td>The line is accepted, but there's no confirmed polling behind it. Use a fixed wait, then validate</td></tr><tr><td><code>Wait until the dashboard loads</code></td><td>Same. Replace with <code>Wait Until 5 Seconds</code></td></tr><tr><td>No wait after <code>OPEN_APP</code></td><td>The first tap fires at the splash screen and resolves to nothing</td></tr><tr><td>No wait between a tap that navigates and the validation after it</td><td>The validation runs against the previous screen, burns all 3 attempts and fails</td></tr><tr><td><code>Wait Until 30 Seconds</code> sprinkled through a suite</td><td>A 40-step test now takes ten minutes longer, and the real timing bug is still there</td></tr><tr><td>A wait longer than 10 seconds</td><td>The step before it did not complete, or the screen itself needs raising with the app team</td></tr><tr><td>Removing a wait because "the validation retries will handle it"</td><td>Retries cover jitter, not latency</td></tr><tr><td>Waiting <em>after</em> the validation instead of before it</td><td>The validation has already failed. Waits go before the step that needs the screen</td></tr></tbody></table>

## Next

* [Validate](/writing-tests/tap/validate) — retries, and what to assert once the screen arrives
* [The shape of a test](/writing-tests/writing-tests) — where waits sit in the five beats
* [Command index](/writing-tests/command-index) — every command in one table

***

*Last updated: 6 August 2026*


# Conditionals

IF / ELSE IF / ELSE blocks let one script survive apps that show different screens to different users.

`IF` blocks run steps only when a condition holds on screen. One script covers apps that show different screens to different users.

|                 |                                                                             |
| --------------- | --------------------------------------------------------------------------- |
| **Platforms**   | Android · iOS                                                               |
| **Branches**    | `IF` → `ELSE IF` → `ELSE`                                                   |
| **Braces**      | `{` and `}` each go on their own line                                       |
| **Indentation** | Required, not cosmetic                                                      |
| **Watch out**   | Prefix the condition with `Validate` — a bare `IF` classifies less reliably |

## Prerequisites

* A connected device or emulator
* An open test file
* A condition that is checkable on a single screen

## Copy this

```
OPEN_APP com.shopease.android
Wait Until 5 Seconds

IF Validate that the location permission dialog is visible
{
    Tap on While using the app
    Wait Until 2 Seconds
}

Tap on the search icon
Type running shoes in the search field
Validate that search results are visible
```

## Write an IF block

1. Write `IF Validate <condition>` on its own line.
2. Put the opening brace `{` on the next line, on its own.
3. Indent the body. Write one action per line.
4. Close with `}` on its own line.
5. Add `ELSE IF` and `ELSE` branches for the other states the screen can be in.
6. Confirm every branch leaves the app on the same screen, so the steps after the block work in all cases.
7. Run the test and confirm the report shows which branch was taken.

## Branches

| Keyword               | When it runs                                    | Condition                                             |
| --------------------- | ----------------------------------------------- | ----------------------------------------------------- |
| `IF <condition>`      | The condition holds                             | Required. Prefix with `Validate` for a presence check |
| `ELSE IF <condition>` | The `IF` did not match and this condition holds | Required                                              |
| `ELSE`                | No earlier branch matched                       | None                                                  |

## Two branches

```
IF Profile icon is visible
{
    Tap on Profile icon
}
ELSE
{
    Tap on Hamburger menu
}
```

## Three or more branches

```
IF Proceed to Pay is visible
{
    Tap on Proceed to Pay
}
ELSE IF Slide to pay is visible
{
    Drag the slider from left to right
}
ELSE
{
    Scroll down until "Proceed" CTA is visible
    Tap on Proceed
}
```

## Nesting

```
IF Validate that the Employee Attendance screen is displayed
{
    IF Validate that the Clock In button is displayed
    {
        Tap on Clock In
        Wait Until 3 Seconds
    }
    Tap on Close button
}
ELSE
{
    Tap on Skip
}
```

## Syntax rules

<table data-search="false"><thead><tr><th>Rule</th><th>Detail</th></tr></thead><tbody><tr><td><strong>Opening brace on its own line</strong></td><td><code>{</code> never sits at the end of the <code>IF</code> line</td></tr><tr><td><strong>Indent the body</strong></td><td>Indentation is part of the syntax, not formatting</td></tr><tr><td><strong>Prefix presence checks with <code>Validate</code></strong></td><td><code>IF Validate that the permission dialog is visible</code> classifies more reliably than the bare form</td></tr><tr><td><strong>One condition per branch</strong></td><td>A condition that checks two elements is harder to evaluate, and the report can't say which half was false</td></tr><tr><td><strong>Every branch stands on its own</strong></td><td>A branch cannot depend on steps that only run in another branch</td></tr><tr><td><strong>Order branches by likelihood</strong></td><td>Most common state in the <code>IF</code>, alternatives in <code>ELSE IF</code>, the unexpected in <code>ELSE</code></td></tr><tr><td><strong>A false <code>IF</code> costs one screen check</strong></td><td>It does not fail the step</td></tr></tbody></table>

## Popups that can appear anywhere

Promotional overlays, permission prompts, "rate this app" and network retry sheets can interrupt any test at any point. They belong in the app's blocker rules in **Memory**, written once. Drizz dismisses them during any run, with no step in the script.

`IF` covers state tied to one screen or one scenario:

```
IF Address Suggestion dropdown is visible
{
    Tap on the first suggestion
}

IF minimum order value popup is visible
{
    Tap on OK
    Wait Until 1 Seconds
}
```

See [Memory & blockers](/writing-tests/memory-and-blockers) for the split, and for how to write a blocker rule.

## Common mistakes

<table data-search="false"><thead><tr><th>What you wrote</th><th>What happens</th></tr></thead><tbody><tr><td><code>IF the popup is visible {</code> — brace on the same line</td><td>The block isn't parsed as a block</td></tr><tr><td>An unindented body</td><td>Same. Indentation is part of the syntax</td></tr><tr><td>An <code>IF</code> for the same promo popup, copied into 30 tests</td><td>The popup still interrupts the tests you forgot. One blocker rule in Memory covers all of them</td></tr><tr><td>A condition that checks two things at once</td><td>Harder to evaluate, and the report can't tell you which half was false</td></tr><tr><td><code>IF</code> with a body that leaves the app on a different screen than the <code>ELSE</code> body does</td><td>The steps after the block work in one branch and fail in the other</td></tr><tr><td>No <code>ELSE</code> on a screen that has a third variation you haven't seen</td><td>The script silently does nothing and fails further down. Add a fallback</td></tr><tr><td>A blocker rule in Memory <em>and</em> an <code>IF</code> for the same popup</td><td>The rule dismisses it, then the <code>IF</code> finds nothing and the branch is skipped — noise in every report</td></tr></tbody></table>

## Next

* [Memory & blockers](/writing-tests/memory-and-blockers) — handle anywhere-popups once
* [Validate](/writing-tests/tap/validate) — writing the condition
* [Modules](/writing-tests/modules) — extract a large branch into its own module

***

*Last updated: 6 August 2026*


# Variables & data

The four mechanisms that hold a value in Drizz — Store, SET, dataset variables and placeholders — compared in one table.

Drizz holds values in four mechanisms:  `SET` dataset variables and placeholders.

|               |                                                                                                            |
| ------------- | ---------------------------------------------------------------------------------------------------------- |
| **Platforms** | Android · iOS                                                                                              |
| **Naming**    | `snake_case`, case-sensitive                                                                               |
| **Lifetime**  | `SET` last one run. Dataset values come from outside the run                                               |
| **Reserved**  | `ctx` — `{{ctx.*}}` is set by Drizz and you can't assign to it                                             |
| **Watch out** | A dataset variable that shows **red** in the editor doesn't exist in the bound dataset. That run will fail |

## Prerequisites

None.

## The four mechanisms

|                      | Written as               | Where the value comes from                                                                      | Lives for                    | Reference it as |
| -------------------- | ------------------------ | ----------------------------------------------------------------------------------------------- | ---------------------------- | --------------- |
| **SET**              | `SET city = "Bangalore"` | Assigned in the script — a literal, another variable, an API field, or a structured screen read | The test run                 | `{{city}}`      |
| **Dataset variable** | `{{phone}}`              | A dataset, bound when the test plan runs                                                        | The run, supplied externally | `{{phone}}`     |

## Selecting a mechanism

Rows apply top to bottom. The first matching row wins.

| Condition                                                                  | Mechanism                | Written as                        |
| -------------------------------------------------------------------------- | ------------------------ | --------------------------------- |
| The value is on the screen now and several fields are needed as one object | `SET` with a screen read | `SET <var> = screen(...)`         |
| The value changes per environment, per account or per run                  | Dataset variable         | `{{var}}`                         |
| The value is fixed for this one test                                       | `SET`                    | `SET <var> = "<literal>"`         |
| The value comes from an API step                                           | `SET`                    | `SET <var> = API.<name>.response` |

A dataset variable is what makes one script run against staging and production, or across ten user accounts. Syntax for `SET` is on [Store & SET](/writing-tests/which-variable/store-and-set); dataset authoring is on [Datasets & test data](/writing-tests/which-variable/datasets).

All four mechanisms in one ShopEase test:

```
# ShopEase — apply a coupon and check the total drops

OPEN_APP {{app_package}}
Wait Until 5 Seconds

Type {{phone}} in the mobile number field
Tap on Get OTP
Type {{otp}} in the OTP field
Tap on Continue
Wait Until 5 Seconds

Tap on Cart
Wait Until 2 Seconds


Type {{coupon_code}} in the coupon field
Tap on Apply
Wait Until 3 Seconds
```

`{{app_package}}`, `{{phone}}` and `{{otp}}` come from a dataset bound to the plan.

## Naming

| Rule                | Detail                                                                                                                                                                                |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Case                | `snake_case` for every mechanism — `subtotal_amount`, `customer_name`, `app_package`                                                                                                  |
| Case sensitivity    | Dataset variable names must match the dataset key exactly. `{{Phone}}` and `{{phone}}` are two different variables                                                                    |
| Reserved namespace  | `ctx` is reserved. `{{ctx.*}}` is populated by Drizz on every run, including inside modules. `SET ctx…` is rejected                                                                   |
| One style per value | A value that lives in a dataset is always `{{phone}}`, never `<phone>`. A placeholder is inert text — Drizz types the literal characters `<phone>` into the field and the step passes |

## Common mistakes

<table data-search="false"><thead><tr><th>What you write</th><th>What happens</th></tr></thead><tbody><tr><td><code>&#x3C;phone></code> for a value that's in the dataset</td><td>Drizz types the literal text <code>&#x3C;phone></code>. The step passes and the login fails</td></tr><tr><td><code>{{Phone}}</code> when the dataset key is <code>phone</code></td><td>Renders red in the editor, resolves to nothing at run time</td></tr><tr><td>Reusing one variable name for two different captures</td><td>The second write overwrites the first, permanently</td></tr><tr><td>A placeholder for a value managed centrally</td><td>It has to be edited by hand before every run. Use a dataset variable</td></tr></tbody></table>

## Next

* [Store & SET](/writing-tests/which-variable/store-and-set)
* [Datasets & test data](/writing-tests/which-variable/datasets)
* [Modules](/writing-tests/modules)

***

*Last updated: 6 August 2026*


# Variables

Capture a value from the screen with Store, or assign one with SET. The four right-hand sides SET accepts, and what it rejects.

`Store` captures what's on the screen. `SET` assigns a value already available to the script. Both last for one test run.

|                            |                                                                     |
| -------------------------- | ------------------------------------------------------------------- |
| **Platforms**              | Android · iOS                                                       |
| **`SET` right-hand sides** | `{{variable.path}}` · `API.<name>.…` · `screen(...)`                |
| **Reference later as**     | `{{var}}`, and `{{var.field}}` for a `screen(...)` object           |
| **Watch out**              | Writing the same name twice overwrites the first value, permanently |

## Prerequisites

* A test open in the desktop app editor
* The value visible on screen, for `Store` and `screen(...)`
* An API registered under `<name>`, for `SET <var> = API.<name>.…`

## The mechanism

{% tabs %}
{% tab title="SET" %}
Assigns a value the script already has.

```
SET city = "Bangalore"
SET total = {{order.amount}}
SET cart = API.shopease_cart.response
SET item_count = API.shopease_cart.response.items.length
SET promo = screen(the promo banner headline on the home screen)
```

**Use for:** a value fixed for this test, a value from an API step, or several related fields captured as one object.

`SET` accepts exactly four kinds of right-hand side. Anything else fails the step with an explicit error.

| # | Right-hand side      | Example                                                                                      | Use when                                        |
| - | -------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| 1 | A literal            | `SET city = "Bangalore"` · `SET qty = 3` · `SET body = { "key": "value" }`                   | The value is fixed for this test                |
| 2 | Another variable     | `SET total = {{order.amount}}`                                                               | The value is already in a variable              |
| 3 | An API reference     | `SET cart = API.shopease_cart.response` · `.request` · `.status_code` · plus any dotted path | The value comes from an API step                |
| 4 | A `screen(...)` read | `SET promo = screen(the promo banner headline)`                                              | Several related fields are needed as one object |

`SET`  accepts AI expressions.&#x20;

```
SET random_city = ai(pick a random city in India).
```

{% endtab %}
{% endtabs %}

## SET … = screen(...)

`screen(...)` returns an object indexable field by field, unlike `Store`, which returns one value.

```
SET product = screen(the product detail card. format={name: "", price: "", rating: ""}. price is the amount including the currency symbol)

Validate {{product.name}} is visible on the screen
Tap on Add to Cart
Wait Until 2 Seconds

Tap on Cart
Validate that {{product.name}} is visible in the cart
Validate that {{product.price}} is visible beside the item
```

A `screen(...)` read takes three parts:

| Part                | Purpose                                           | Example                                             |
| ------------------- | ------------------------------------------------- | --------------------------------------------------- |
| Surface description | Names the region of the screen to read            | `the product detail card`                           |
| `format={…}`        | The keys to return. `[]` marks a list-valued key  | `format={item_count: "", total: "", items: []}`     |
| Key hint            | Disambiguates any key whose meaning isn't obvious | `price is the amount including the currency symbol` |

```
SET cart_summary = screen(the cart summary panel. format={item_count: "", total: "", items: []}. items is the list of product names shown as rows)
```

## Overwrites are permanent

Variables are a single key/value store. Writing the same name twice replaces the first value, and the original is gone.

## Using and validating a stored value

Reference a stored value anywhere a value goes:

```
Type {{otp_initial}} in the OTP field
Type {{ref_code}} in the referral code box
Type {{order_id}} in the order search field
```

And validate it:

```
Validate that {{order_id}} is visible on the confirmation screen
Validate that {{total_after}} is greater than {{total_before}}
Validate that {{updated_stock}} equals {{current_stock}} plus 10
```

Numeric comparisons do real arithmetic — `greater than`, `less than`, `equals … plus …`. Directional comparisons survive price and tax changes; an exact expected number does not.

## A failed SET may not stop the test

A `SET` step that fails to evaluate can report as passed. The run continues with the variable unset, and later steps act on an empty value.

To catch it, add a validation after any `SET` the rest of the test depends on:

1. Write the `SET` step.

   ```
   SET product = screen(the product detail card. format={name: "", price: ""})
   ```
2. Validate one field of the result on the next line.

   ```
   Validate {{product.name}} is visible on the screen
   ```
3. Run the test and confirm the failure now lands on the `SET`, not three steps later.

## Common mistakes

<table data-search="false"><thead><tr><th>What you write</th><th>What happens</th></tr></thead><tbody><tr><td><code>Validate that order_id is visible</code></td><td>Looks for the literal text <code>order_id</code> on screen. Write <code>{{order_id}}</code></td></tr><tr><td><code>Store the order ID as id</code> when it's only in the backend</td><td>Vision AI reads pixels. It can't see values the app doesn't render</td></tr><tr><td><code>Store the price as p</code> then <code>Type p in the search field</code></td><td>Types the letter <code>p</code>. Reference it as <code>{{p}}</code></td></tr><tr><td>Storing a value that's scrolled off-screen</td><td>Nothing to read. Scroll to it first</td></tr></tbody></table>

## Next

* [Which variable do I use?](/writing-tests/which-variable)
* [API steps](/writing-tests/api-steps)
* [Validate](/writing-tests/tap/validate)

***

*Last updated: 6 August 2026*


# Datasets

Datasets supply {{variable}} values from outside your script, bound when a test plan runs. Same script, different data.

A **dataset** is a named set of values bound to your tests at run time. One script covers many environments and accounts.

|                 |                                                                          |
| --------------- | ------------------------------------------------------------------------ |
| **Platforms**   | Android · iOS                                                            |
| **Authored as** | YAML key/value, in the Datasets area                                     |
| **Types**       | string · number · list · object                                          |
| **Nesting**     | **2 levels overall.** Deeper is rejected on save                         |
| **Watch out**   | Binding happens when a **test plan** runs, not when you write the script |

## Prerequisites

* Drizz desktop app installed and signed in
* A project containing the tests the dataset will drive
* A test plan, to bind the dataset to

## Copy this

The dataset — `shopease_staging`:

```yaml
app_package: com.shopease.android
phone: "9000000000"
otp: "123456"
city: Bangalore
search_term: running shoes
```

The test that uses it:

```
# ShopEase — login and search, driven by a dataset

OPEN_APP {{app_package}}
Wait Until 5 Seconds

Type {{phone}} in the mobile number field
Tap on Get OTP
Wait Until 2 Seconds

Type {{otp}} in the OTP field
Tap on Continue
Wait Until 5 Seconds

Tap on the search icon
Type {{search_term}} in the search field
Tap on the first search result
Wait Until 3 Seconds

Validate that the product detail page is visible
```

A second dataset pointed at production, bound instead, runs the same test there.

## Create and bind a dataset

1. Open the **Datasets** area of the desktop app.
2. Author the dataset as YAML key/value pairs.

   ```yaml
   app_package: com.shopease.android
   phone: "9000000000"
   otp: "123456"
   max_items: 5
   categories: [shoes, bags, watches]
   user:
     email: qa@example.com
   ```
3. Save the dataset. Save rejects more than two levels of nesting.
4. Reference each key in the script as `{{key}}`.
5. Open the test plan that runs those tests and select the dataset on its **Data set** tab.
6. Confirm the unresolved-variable counter reads zero. A plan can't be saved or run while a variable is unresolved.

## Value types

| Type   | Written as                                       | Notes                                                |
| ------ | ------------------------------------------------ | ---------------------------------------------------- |
| String | `city: Bangalore`                                | Quote anything that must stay a string               |
| Number | `max_items: 5`                                   | `otp: 012345` unquoted is read as the number `12345` |
| List   | `categories: [shoes, bags, watches]`             |                                                      |
| Object | `user:` then an indented `email: qa@example.com` | Reference as `{{user.email}}`                        |

## Nesting limit

Nesting is capped at **two levels overall**. This is accepted:

```yaml
user:
  email: qa@example.com
```

This is rejected on save:

```yaml
user:
  address:
    city: Bangalore
```

Flatten a third level into the key — `user_address_city: Bangalore`.

## Referencing a variable

| Form            | Resolves to                                                  |
| --------------- | ------------------------------------------------------------ |
| `{{key}}`       | The value of that key, anywhere a value goes, in any command |
| `{{key.field}}` | A field of an object value                                   |

```
OPEN_APP {{app_package}}
Type {{phone}} in the mobile number field
Validate that {{city}} is visible on the address bar
Type {{user.email}} in the email field
```

Names are **case-sensitive** and must match the dataset key exactly.

## Binding

Binding happens on the **test plan**, not in the script. Every `{{variable}}` in every test in that plan resolves from the bound dataset. The same test case is used by different people testing different cases, so the data belongs to the run rather than the file.

Running a single test from the editor prompts for its variable values for that one run.

A plan that contains variables but has **no test data bound at all** can misread `SET` steps. Bind a dataset — an empty one is enough — to any plan whose tests use variables.

## Editor behavior

| Signal               | Meaning                                                                                                                                                                       |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Green** token      | The variable exists in the selected dataset. Hover shows the bound value                                                                                                      |
| **Red** token        | The variable doesn't exist in the selected dataset. It resolves to nothing, and the run fails several steps later — most often as "element not found"                         |
| Autocomplete on `{{` | Suggests variables visible in the current context. Suggestions are fetched when typing pauses, not on every keystroke. Selecting one places the cursor after the closing `}}` |

Autocomplete isn't a complete list of everything a plan might bind. The dataset is the source of truth.

## Data that can't be shared

Login credentials, phone numbers and accounts mid-flow can't be used by two runs at the same time. Mark the dataset a **lockable pool** so each parallel run leases its own row. See [Lockable pools](/writing-tests/which-variable/lockable-pools).

## Common mistakes

<table data-search="false"><thead><tr><th>What you write</th><th>What happens</th></tr></thead><tbody><tr><td><code>otp: 012345</code> unquoted</td><td>Read as the number 12345. The leading zero is gone</td></tr><tr><td><code>{{Phone}}</code> when the key is <code>phone</code></td><td>Renders red, resolves to nothing</td></tr><tr><td>Three levels of YAML nesting</td><td>Rejected on save. Flatten the key</td></tr><tr><td>Picking a dataset while writing the script</td><td>There's nothing to pick. Binding happens on the plan</td></tr><tr><td><code>OPEN_APP {{shopease_staging}}</code> — the dataset name</td><td>Corrupts the value. Reference the key, not the dataset</td></tr><tr><td>A plan with variables and no dataset bound</td><td><code>SET</code> steps can be misread. Bind one, even an empty one</td></tr></tbody></table>

## Next

* [Lockable pools](/writing-tests/which-variable/lockable-pools)
* [Which variable do I use?](/writing-tests/which-variable)
* [Test plans](/running-tests/test-plans)

***

*Last updated: 6 August 2026*


# Lockable pools

Lease test data that two runs can't share — logins, phone numbers, accounts. One row per run, released at the end.

A **lockable pool** is a dataset of equal-length lists. Each index across the lists is one row, and a running test leases one row for the length of the run.

|               |                                                                        |
| ------------- | ---------------------------------------------------------------------- |
| **Platforms** | Android · iOS                                                          |
| **A row**     | The i-th value of every column, leased as one unit                     |
| **Rule**      | Every column must be a list, and all columns the same length           |
| **Scope**     | Leases are global — honored across every plan, run and tab at once     |
| **Watch out** | Pool size caps parallelism. A 4-row pool means at most 4 tests at once |

## Prerequisites

* Drizz desktop app installed and signed in
* A dataset whose every column is a list of the same length
* A test plan, to lock the variables on

## Copy this

The dataset — `shopease_logins`, with **Lockable pool** ticked:

```yaml
phone: ["9000000001", "9000000002", "9000000003", "9000000004"]
otp:   ["123456", "123456", "123456", "123456"]
```

That is a pool of **4 rows**. Row 0 is `{phone: 9000000001, otp: 123456}`.

The test that leases from it:

```
# ShopEase — login with a leased account

OPEN_APP com.shopease.android
Wait Until 5 Seconds

Type {{phone}} in the mobile number field
Tap on Get OTP
Wait Until 2 Seconds

Type {{otp}} in the OTP field
Tap on Continue
Wait Until 5 Seconds

Validate that the home screen is visible
```

The script is identical to a normal dataset-driven test. The difference is in how the plan resolves `{{phone}}` and `{{otp}}`.

## Data that can't be shared

| Data                            | What happens without a pool                     |
| ------------------------------- | ----------------------------------------------- |
| Login credentials               | A second login can invalidate the first session |
| Phone numbers waiting on an OTP | Two runs race for the same message              |
| Accounts mid-flow               | One run's cart is the other run's mystery bug   |

Ten parallel tests against one account produce ten flaky results. A pool gives each run its own row.

## Set up a pool

1. Write every column as a list, all the same length.

   ```yaml
   phone: ["9000000001", "9000000002", "9000000003", "9000000004"]
   otp:   ["123456", "123456", "123456", "123456"]
   ```
2. Tick **Lockable pool** in the dataset editor.
3. Save the dataset.
4. Open the plan's data step and set each pooled variable's source to **lock to a pool**.
5. Run the plan and open the **Locks** panel to confirm rows are leased and released.

Save rejects a pool that isn't shaped right:

| Problem                      | Message on save                             |
| ---------------------------- | ------------------------------------------- |
| A column isn't a list        | Rejected — it names the offending variables |
| Columns of different lengths | Rejected — it lists the lengths             |
| An empty pool                | Rejected — a pool needs at least one row    |

Single-quoted inline lists (`['a', 'b']`) don't parse as a list and are caught by the not-a-list check. Use double quotes, or block `-` items.

A variable that only a pool provides is locked automatically. A variable that both a pool and a normal dataset provide is left to be chosen — nothing is selected silently. Because a row is paired, locking one column also leases the pool's other referenced columns.

## Lease behavior

<table data-search="false"><thead><tr><th>Stage</th><th>Behavior</th></tr></thead><tbody><tr><td>Acquire</td><td>The test leases the lowest free row when it starts</td></tr><tr><td>Binding</td><td>Every column in that row binds into the run. <code>{{phone}}</code> and <code>{{otp}}</code> come from the same index, always paired</td></tr><tr><td>Hold</td><td>The row is held for the whole run, by at most one run at a time</td></tr><tr><td>Release</td><td>The row is released when the run finishes — passed, failed or timed out</td></tr><tr><td>Scope</td><td>Leases are global. A plan, a second plan, and a single test run from the editor draw from one registry, so no two get the same row</td></tr><tr><td>Exhaustion</td><td>The next run waits rather than failing, with <code>pool_exhausted</code> as the wait reason. It gives up after a timeout and fails with "no free value in pool"</td></tr><tr><td>Stale recovery</td><td>Leases older than the stale threshold are reclaimed the next time anything tries to acquire a row</td></tr></tbody></table>

## Sizing

| Pool rows vs parallelism   | Result                                                                                             |
| -------------------------- | -------------------------------------------------------------------------------------------------- |
| Rows ≥ maximum parallelism | Every run leases immediately                                                                       |
| Rows < maximum parallelism | The extra runs queue. The suite still passes; it takes as long as running at the lower concurrency |
| Rows > maximum parallelism | No cost. Spare rows sit unleased                                                                   |

A plan that runs 10-wide needs a pool of 10 rows or more.

## Several pools at once

A test can lease from more than one pool — credentials from one, phone numbers from another. Lock each variable to its pool.

Rows are acquired all-or-nothing. If one pool is full, whatever was already acquired is released and the run retries, which is what stops two runs deadlocking while each holds half of what it needs.

Each pool independently caps concurrency at its own size. A 2-row credentials pool plus a 2-row phone pool, with six cases needing both, runs at most 2 at a time.

## Rules and limits

* **All columns must be the same length**, or the pool can't be locked or used at all.
* **A `SET` in the test wins** over a leased value for the same variable. A stray `SET phone = "9000000000"` silently defeats the pool.
* **Locking gives&#x20;*****different*****&#x20;values, not the same one.** Two runs that must use an identical value take a fixed value from a normal dataset instead.
* **Data-driven looping and lockable pools can't be combined** on one plan.
* **Deleting or renaming a pool a saved plan uses** stops that plan running until the plan is reopened and pools are chosen again. It never silently leases from somewhere else.
* **Two selected pools providing the same variable** is flagged as a conflict and blocks save and run. Untick all but one.

## When rows get stuck

A run that dies without releasing — a closed tab, a crashed session — leaves its row held. Two mechanisms recover it:

* Leases older than the stale threshold are reclaimed automatically the next time anything tries to acquire a row.
* The **Locks** panel has **Clear stale** and **Release all**, plus a per-row release.

Releasing a row that's still running frees it for reuse while the run continues, so two runs can then share that value. The panel marks running rows and asks for confirmation.

## Common mistakes

<table data-search="false"><thead><tr><th>What you write</th><th>What happens</th></tr></thead><tbody><tr><td><code>phone</code> as a list, <code>otp</code> as a single value</td><td>The pool can't be locked. Every column must be a list</td></tr><tr><td>Columns of different lengths</td><td>Rejected on save. Even a one-row difference blocks the pool</td></tr><tr><td><code>phone: ['9000000001']</code> — single quotes inline</td><td>Doesn't parse as a list. Use double quotes or block <code>-</code> items</td></tr><tr><td>A 4-row pool with concurrency set to 10</td><td>Six runs sit waiting. You'll see <code>pool_exhausted</code></td></tr><tr><td><code>SET phone = "9000000000"</code> in a test that leases <code>phone</code></td><td>The <code>SET</code> wins and every parallel run uses the same number</td></tr><tr><td>Expecting two runs to get the same leased row</td><td>They never will. Use a fixed value instead</td></tr><tr><td>Renaming a pool a saved plan uses</td><td>The plan won't run until you reopen it and pick pools again</td></tr></tbody></table>

## Next

* [Datasets & test data](/writing-tests/which-variable/datasets)
* [Test plans](/running-tests/test-plans)
* [Devices: local, cloud & private](/running-tests/devices)

***

*Last updated: 6 August 2026*


# Modules

Write a flow once and call it from every test. Module definition, CALL, parameters, and the limits that apply to them.

A **module** is a named script fragment called from any test with `CALL`. A flow repeated across tests — login, choosing a city, setting up an address — is written once as a module.

|                 |                                                                                  |
| --------------- | -------------------------------------------------------------------------------- |
| **Platforms**   | Android · iOS                                                                    |
| **Define with** | `# BEGIN <name>` … `# END <name>`                                                |
| **Parameters**  | `PARAM <name>` in the body, `CALL name(param={{value}})` at the call site        |
| **Nesting**     | Allowed. Cyclic calls are rejected                                               |
| **Watch out**   | After typing `CALL`, pause for the dropdown — typing straight through outruns it |

## Prerequisites

* Drizz desktop app installed and signed in
* A project to hold the module file
* A dataset bound to the plan, for any `{{variable}}` the module body references

## Copy this

The module, saved as its own file:

```
# BEGIN shopease_login
OPEN_APP {{app_package}}
Wait Until 5 Seconds

Type {{phone}} in the mobile number field
Tap on Get OTP
Wait Until 2 Seconds

Type {{otp}} in the OTP field
Tap on Continue
Wait Until 5 Seconds

Validate that the home screen is visible
# END shopease_login
```

The test that uses it:

```
# T04 — ShopEase: search and add to cart

CALL shopease_login

Tap on the search icon
Wait Until 2 Seconds

Type running shoes in the search field
Tap on the first search result
Wait Until 3 Seconds

Validate that the product detail page is visible

Tap on Add to Cart
Wait Until 2 Seconds

Validate that Item added to cart is visible

CLEAR_APP com.shopease.android
```

## Create and call a module

1. Create a script file for the module.
2. Save\<Select Module
3. Name the module in `snake_case`, for what it does — `shopease_login`, `city_selection_from_home`.
4. Open the test that needs it and type `CALL` on its own line.
5. Pause for the module dropdown, then select the module by name.
6. Run the test and confirm the module's steps appear in the report.

Anything valid in a test is valid in a module, including `IF` blocks, `Validate` and other `CALL`s. A module doesn't have to start with `OPEN_APP` — a module that picks a city or dismisses onboarding doesn't launch the app.

## CALL forms

| Form               | Written as                            | Behavior                                                         |
| ------------------ | ------------------------------------- | ---------------------------------------------------------------- |
| Plain call         | `CALL shopease_login`                 | Runs the module body in place                                    |
| Parameterized call | `CALL shopease_search(term={{food}})` | Binds each named argument to the matching `PARAM` before running |
| Nested call        | A `CALL` inside a module body         | Allowed to any depth. Cyclic calls are rejected                  |

The module's body appears inlined under the call line in the editor, indented and **read-only**:

```
CALL shopease_login
  # BEGIN shopease_login
  OPEN_APP {{app_package}}
  Wait Until 5 Seconds
  Type {{phone}} in the mobile number field
  # END shopease_login
```

| Behavior of the inlined block     | Detail                                                                               |
| --------------------------------- | ------------------------------------------------------------------------------------ |
| Editable                          | No. Edit the module file and every caller updates                                    |
| Enter at the end of the call line | Jumps past the block, so typing continues in the calling script                      |
| Deleting                          | Removes the whole block as a unit. It can't be half-deleted                          |
| At run time                       | Drizz collapses the expansion before running, so the module executes once, not twice |

## Parameters

```
SET food = "Burger"
CALL shopease_search(term={{food}})
  # BEGIN shopease_search
  PARAM term
  Tap on the search icon
  Type {{term}} in the search field
  # END shopease_search
```

| Element                   | Rule                                                                                                                                                                             |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Declaration               | `PARAM <name>` on its own line, immediately after `# BEGIN`                                                                                                                      |
| Reference in the body     | `{{name}}`                                                                                                                                                                       |
| Argument at the call site | Named: `CALL shopease_search(term={{food}})`                                                                                                                                     |
| Autocomplete              | Selecting a module inserts its parameter signature with the caret in the first slot                                                                                              |
| Passing semantics         | By value. A module that assigns a variable does not change the caller's copy. A module that sets `total` internally leaves the caller's `total` untouched after the call returns |

## Rules and limits

<table data-search="false"><thead><tr><th>Rule</th><th>Detail</th></tr></thead><tbody><tr><td>Naming</td><td><code>snake_case</code>, for what the module does. Not <code>T01_module</code></td></tr><tr><td>Scope</td><td>One purpose per module. A module that logs in <em>and</em> navigates forces the next caller to copy it to get half of it</td></tr><tr><td>Nesting</td><td>A module can call another module. Cyclic calls, which would loop forever, are rejected</td></tr><tr><td>Definitions</td><td>Every <code>CALL</code> needs a matching definition. Deleting the project that holds a module breaks every test calling it, and the warning is weak</td></tr><tr><td>Autocomplete lag</td><td>Type <code>CALL</code>, then pause for the module dropdown. Typing the name straight through outruns it and produces a <code>CALL</code> that looks right but isn't linked</td></tr><tr><td>Not a module</td><td>A single <code>Tap</code>. The indirection costs more than it saves</td></tr><tr><td>Not a module</td><td>A popup that can appear anywhere. Those go in <a href="/pages/wl2GqBAWrbDmrlNZGHGT">Memory</a> once and need no step in any script</td></tr></tbody></table>

## Common mistakes

<table data-search="false"><thead><tr><th>What you write</th><th>What happens</th></tr></thead><tbody><tr><td><code>CALL shopease_login</code> typed fast, no pause</td><td>The dropdown never fires and the call isn't linked to a module</td></tr><tr><td>Editing the inlined block under a <code>CALL</code></td><td>It's read-only. Edit the module file</td></tr><tr><td><code># BEGIN login</code> … <code># END shopease_login</code></td><td>Names don't match — the module won't parse</td></tr><tr><td>A module that calls itself, directly or via another</td><td>Rejected as a cyclic call</td></tr><tr><td>Deleting the project that holds a shared module</td><td>Every test calling it breaks, Drizz warns you and blocks deletion until modules are resolved.</td></tr><tr><td><code>PARAM term</code> halfway down the body</td><td>Declare parameters immediately after <code># BEGIN</code></td></tr><tr><td>Expecting a module's <code>SET</code> to change the caller's variable</td><td>Parameters and variables are pass-by-value. It won't</td></tr><tr><td>One module that logs in <em>and</em> navigates to checkout</td><td>The next test needs half of it and copies the whole thing</td></tr></tbody></table>

## Next

* [The shape of a test](/writing-tests/writing-tests)
* [Which variable do I use?](/writing-tests/which-variable)
* [Memory & blockers](/writing-tests/memory-and-blockers)

***

*Last updated: 6 August 2026*


# API

Call a registered API mid-test to seed data, fetch an expected value, or check the UI against the backend.

Drizz calls a registered API in the middle of a test. Register the API once, then call it by name with `API: <name>`.

|                    |                                                                                  |
| ------------------ | -------------------------------------------------------------------------------- |
| **Platforms**      | Android · iOS                                                                    |
| **Register first** | Editor → API → paste a cURL command → save under a name                          |
| **Response refs**  | `API.<name>.response` · `.request` · `.status_code` · plus dotted paths          |
| **Position**       | An API step may come before `OPEN_APP`                                           |
| **Watch out**      | "API file not found" means the name in the script doesn't match a registered API |

## Prerequisites

* Drizz desktop app installed and signed in
* A working cURL command for the endpoint, including headers
* The internal flag enabled on the API, for endpoints reachable only from your own network

## Copy this

```
# ShopEase — the cart total on screen matches the backend

API: shopease_cart
  # BEGIN shopease_cart
  {
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer <your_token>"
    }
  }
  # END shopease_cart

SET cart_total = API.shopease_cart.response.total
SET item_count = API.shopease_cart.response.items.length

OPEN_APP com.shopease.android
Wait Until 5 Seconds

Tap on Cart
Wait Until 2 Seconds

Validate that the Order Total on screen matches {{cart_total}}
Validate that the number of items in the cart matches {{item_count}}
```

## Register an API

APIs are registered, not written inline. A registered name is what the script calls, and a cURL command carries the headers across without retyping.

1. Open **Editor → API** in the desktop app.
2. Paste a cURL command for the endpoint.
3. Enter a name in the **save as** field — `shopease_cart`.
4. Save the API.
5. Write `API: shopease_cart` in a test and run it. Confirm the step reports a status code.

## Calling it

```
API: shopease_cart
  # BEGIN shopease_cart
  { "headers": { "Content-Type": "application/json" }, "body": { "user_id": "42" } }
  # END shopease_cart
```

The request block sits indented under the call line, between `# BEGIN <name>` and `# END <name>`, and holds JSON. Headers and body set here override the registered values for this test.

API blocks stay expanded, unlike module blocks. The payload is part of the test.

`API:` is the current keyword. `EXECUTE_API:` is the legacy form and still runs, so old scripts keep working. Write `API:` in anything new.

## Using the response

| Reference                    | Gives you                     |
| ---------------------------- | ----------------------------- |
| `API.<name>.response`        | The whole response body       |
| `API.<name>.response.<path>` | Any dotted path into the body |
| `API.<name>.request`         | The request that was sent     |
| `API.<name>.status_code`     | The HTTP status code          |

```
SET cart = API.shopease_cart.response
SET item_count = API.shopease_cart.response.items.length
SET first_item = API.shopease_cart.response.items.0.name

Validate that {{first_item}} is visible in the cart
```

Assign a reference to a variable with `SET`, then reference the variable as `{{var}}`. See [Store & SET](/writing-tests/which-variable/store-and-set) for what else `SET` accepts.

## Where the call runs from

{% tabs %}
{% tab title="Cloud (default)" %}
API calls go out from Drizz's cloud. No configuration is needed.

A `localhost` or LAN endpoint called from the cloud times out after roughly 24 seconds.

{% hint style="warning" %}
A local stack requires the internal flag on that API. Without it the call leaves from the cloud and cannot reach the endpoint.
{% endhint %}
{% endtab %}

{% tab title="Internal network" %}
An API flagged **internal** is routed through your desktop app. The request leaves from your own machine and hits your own DNS, so VPN-only and internal-network endpoints work without allowlisting any Drizz IPs.

The flag is set **per API**, not per organization, so one test can hit a public endpoint and an internal one. Ask Drizz support to enable it on a given API.
{% endtab %}
{% endtabs %}

## Check the UI against the backend

Assert that what the app shows matches what the backend returned.

1. Launch the app and reach the screen.

   ```
   OPEN_APP com.shopease.android
   Wait Until 5 Seconds
   Tap on Cart
   Wait Until 2 Seconds
   ```
2. Call the API.

   ```
   API: shopease_cart
     # BEGIN shopease_cart
     { "headers": { "Content-Type": "application/json" } }
     # END shopease_cart
   ```
3. Check the call succeeded.

   ```
   Validate that API.shopease_cart.status_code equals 200
   ```
4. Compare the response to the screen.

   ```
   SET cart_total = API.shopease_cart.response.total
   Validate that the Order Total on screen matches {{cart_total}}
   ```

## Seed data before the app opens

An API step can run before `OPEN_APP`, which sets a known state without clicking through the UI to build it.

```
# ShopEase — the promo banner shows for an eligible account

API: shopease_grant_promo
  # BEGIN shopease_grant_promo
  { "headers": { "Content-Type": "application/json" }, "body": { "phone": "9000000000", "promo": "SHOPEASE10" } }
  # END shopease_grant_promo

Validate that API.shopease_grant_promo.status_code equals 200

OPEN_APP com.shopease.android
Wait Until 5 Seconds

Type 9000000000 in the mobile number field
Tap on Get OTP
Type 123456 in the OTP field
Tap on Continue
Wait Until 5 Seconds

Validate that the SHOPEASE10 promo banner is visible
```

## Location-specific content

Fetch what the backend returns for a location, then check the app agrees.

```
# ShopEase — offers for Bangalore match the backend

SET_GPS(latitude=12.9716, longitude=77.5946)

API: shopease_offers
  # BEGIN shopease_offers
  { "headers": { "Content-Type": "application/json" }, "body": { "latitude": 12.9716, "longitude": 77.5946 } }
  # END shopease_offers

SET offer_count = API.shopease_offers.response.offers.length
SET top_offer = API.shopease_offers.response.offers.0.title

OPEN_APP com.shopease.android
Wait Until 5 Seconds

Tap on Offers
Wait Until 3 Seconds

Validate that {{top_offer}} is visible on the offers screen
Validate that the number of offers shown matches {{offer_count}}
```

## Common mistakes

<table data-search="false"><thead><tr><th>What you write</th><th>What happens</th></tr></thead><tbody><tr><td><code>API: shopease_cart</code> when it's registered as <code>shopease-cart</code></td><td>"API file not found". The names must match exactly</td></tr><tr><td>An API step with no registered API behind it</td><td>"API file not found" — register it first</td></tr><tr><td><code># BEGIN shopease_cart</code> … <code># END cart</code></td><td>Names don't match, the block doesn't parse</td></tr><tr><td>Editing the request block expecting it to collapse</td><td>API blocks stay expanded by design. That's not a bug</td></tr><tr><td><code>EXECUTE_API: get_cart(user="42")</code> in a new test</td><td>Legacy form. Still runs, but write <code>API:</code> and put args in the block</td></tr><tr><td>Calling a <code>localhost</code> endpoint without the internal flag</td><td>The call leaves from the cloud and times out after ~24s</td></tr><tr><td><code>SET total = API.shopease_cart.response.total</code> with no status check</td><td>A 500 gives you an empty value and a confusing failure three steps later</td></tr></tbody></table>

## Next

* [Store & SET](/writing-tests/which-variable/store-and-set)
* [Validate](/writing-tests/tap/validate)
* [Recipes](/writing-tests/recipes)

***

*Last updated: 6 August 2026*


# Memory & Blockers

Memory is org-level context attached to your app. Blocker rules dismiss popups automatically, with no step in any script.

**Memory** is org-level context attached to a registered app. **Blocker rules** are its main content — a condition and one recovery action, written once, applied to every run.

|                |                                                                               |
| -------------- | ----------------------------------------------------------------------------- |
| **Platforms**  | Android · iOS                                                                 |
| **Scope**      | The app, org-wide. Every test everyone runs against that package              |
| **Linked to**  | A registered app. Multiple package names can map to one entry                 |
| **Hard limit** | **One action per rule.** A rule cannot do X then Y                            |
| **Watch out**  | A rule that dismisses something a test is meant to check will hide a real bug |

## Prerequisites

* Drizz desktop app installed and signed in
* An app registered through the web app
* The exact on-screen text of the popup being dismissed

## Copy this

Blocker rules for ShopEase. Each is a condition and a single recovery action.

```
blocker: A promotional or upsell bottom sheet is visible unexpectedly.
recovery: Tap the Close ("X") CTA.

blocker: "Your internet is a little wonky" with a Retry button.
recovery: Tap on Retry

blocker: Permission prompt for location or notifications is visible.
recovery: Tap "While using the app"

blocker: "Rate ShopEase" bottom sheet is visible over the home or order screen.
recovery: Tap "Not Now".

blocker: First time user experience flow with a "Continue to [section]" button (e.g. "Continue to Deals").
recovery: Tap on the "Continue to [section]" button
```

With those five in Memory, no ShopEase test needs an `IF` block for any of them.

## What Memory holds

Memory holds context about an app that makes Vision AI more accurate on it: blocker rules, plus app-specific hints.

| Property        | Detail                                                                                                            |
| --------------- | ----------------------------------------------------------------------------------------------------------------- |
| Scope           | An app, not a test. Everything in it applies to every run against that package                                    |
| Link            | A registered app — one uploaded through the web app                                                               |
| Package mapping | Multiple package names can map to one entry, which covers debug, staging and production builds with different IDs |
| Sharing         | Shared across the whole organization. Every member's tests get it                                                 |
| Lifetime        | Until it is changed                                                                                               |

## Memory v/s Variables

|                                 | What it is                                               | Where it lives                                | How long it lasts   |
| ------------------------------- | -------------------------------------------------------- | --------------------------------------------- | ------------------- |
| **Memory** (this page)          | Org-level context about an app — blocker rules and hints | The Memory area, attached to a registered app | Until you change it |
| **Variables** (`Store` / `SET`) | Values a test captures or assigns while it runs          | Inside one test run                           | That run only       |

These docs say **Memory** for the app context and **variables** for the other. `Store the Order Total as total` belongs to [Store & SET](/writing-tests/which-variable/store-and-set).

## Blocker rules

Each rule is a **condition** and a **single recovery action**. Drizz applies it during any run, with no step in any script.

```
blocker: A half card with a close (X) button offering size or color variants of the item.
recovery: Tap on "Add Item" CTA

blocker: Popup with "Replace cart item?"
recovery: Tap on "Yes"

blocker: "Select Delivery Address" sheet is open, dimming the address dropdown at the top.
recovery: Tap the first visible address

blocker: in-app notification with a "Got it" button is visible
recovery: Tap on "Got it"
```

A rule fires only when Drizz decides the screen is blocked. It isn't a background clicker — a matching popup that isn't in the way is left alone.

### Constraints on a rule

<table data-search="false"><thead><tr><th>Constraint</th><th>Detail</th></tr></thead><tbody><tr><td>One action per rule</td><td>Hard limit. A rule cannot do X and then Y. Recovery that needs two steps belongs in an <code>IF</code> block in the script</td></tr><tr><td>Appending a wait</td><td>The one accepted extension of a single action: <code>recovery: Tap "Add separately" CTA and wait for 2 seconds.</code></td></tr><tr><td>Exact text</td><td>Quote the exact on-screen text in the condition. Vague conditions over-match and dismiss things they shouldn't</td></tr><tr><td>Nothing under test</td><td>A rule that closes a dialog a test validates — "Payment failed" — turns a real bug into a green run</td></tr><tr><td>Org-wide effect</td><td>A rule affects every test the whole organization runs against that package. A popup that appears in one flow only belongs in an <code>IF</code> block</td></tr><tr><td>No duplication</td><td>A blocker in Memory must not also be an <code>IF</code> block in scripts. One fires, the other's condition is false, and the script reads as if Memory isn't working</td></tr><tr><td>Generic over specific</td><td>One pattern rule replaces five near-identical rules</td></tr></tbody></table>

| Don't                         | Do                                                                       |
| ----------------------------- | ------------------------------------------------------------------------ |
| `blocker: a popup is visible` | `blocker: "Rate ShopEase" bottom sheet is visible over the home screen.` |
| `blocker: an error appears`   | `blocker: "Your internet is a little wonky" with a Retry button.`        |

A generic pattern rule:

```
blocker: First time user experience flow with a "Continue to [section]" button (e.g. "Continue to Deals/Grocery/Fashion").
recovery: Tap on the "Continue to [section]" button
```

## Memory rule v/s IF block

{% tabs %}
{% tab title="Memory blocker rule" %}
**Use when** the popup can appear in any test, at any point.

```
blocker: "Rate ShopEase" bottom sheet is visible over the home or order screen.
recovery: Tap "Not Now".
```

|                     |                                                               |
| ------------------- | ------------------------------------------------------------- |
| Written in          | The Memory area, against a registered app                     |
| Applies to          | Every test in the organization that runs against that package |
| Actions allowed     | One                                                           |
| Steps in the script | None                                                          |
| {% endtab %}        |                                                               |

{% tab title="Inline IF block" %}
**Use when** the state is tied to one screen or one scenario.

```
IF Address Suggestion dropdown is visible
{
    Tap on the first suggestion
}

IF minimum order value popup is visible
{
    Tap on OK
    Wait Until 1 Seconds
}

IF maximise button is visible
{
    Tap on maximise button
    Wait Until 2 Seconds
}
```

|                     |                               |
| ------------------- | ----------------------------- |
| Written in          | The test script               |
| Applies to          | That test only                |
| Actions allowed     | Any number                    |
| Steps in the script | One block per popup, per test |

The `maximise button` block belongs in every test that runs on tablets. Parallel runs can reset orientation to portrait.
{% endtab %}
{% endtabs %}

## Common mistakes

<table data-search="false"><thead><tr><th>What you write</th><th>What happens</th></tr></thead><tbody><tr><td><code>recovery: Tap Close then tap Continue</code></td><td>Two actions. One action per rule — this is a hard limit</td></tr><tr><td><code>blocker: a bottom sheet is visible</code></td><td>Over-matches. It starts dismissing sheets your tests need</td></tr><tr><td>A rule that closes the "Payment failed" dialog</td><td>The test that checks for that error passes forever</td></tr><tr><td>The same popup in Memory <em>and</em> in an <code>IF</code> block</td><td>Confusing to read, and hides whether Memory is working</td></tr><tr><td>A rule for a popup only your one test sees</td><td>Every test in the org now pays for a check it doesn't need</td></tr><tr><td>Expecting Memory to fix a flaky tap</td><td>Memory dismisses blockers. It doesn't improve targeting — fix the description</td></tr><tr><td>Confusing Memory with <code>Store</code></td><td>Different features, same word. See the table above</td></tr></tbody></table>

## Next

* [Conditionals](/writing-tests/conditionals)
* [Store & SET](/writing-tests/which-variable/store-and-set)
* [Authoring rules](/writing-tests/authoring-rules)

***

*Last updated: 6 August 2026*


# Authoring rules

The rule list for writing Drizz tests — what to do and what to avoid, with the reason each rule exists.

The rules that apply to every Drizz test, in two tables.

|               |                                                                              |
| ------------- | ---------------------------------------------------------------------------- |
| **Platforms** | Android · iOS                                                                |
| **Structure** | Setup → navigate → act → validate → clean up                                 |
| **Wording**   | Use the app's exact visible text, always                                     |
| **Timing**    | Fixed waits only, placed after navigation and network calls                  |
| **Watch out** | `CLEAR_APP` is effectively a no-op on iOS — don't rely on it for clean state |

## Prerequisites

* Drizz desktop app installed and signed in
* A registered app and a project to hold the tests

## Do

<table data-search="false"><thead><tr><th>Rule</th><th>Why it matters</th></tr></thead><tbody><tr><td>One scenario per test, ending in <code>CLEAR_APP</code></td><td>A failure names one feature, and the next test starts from a known state</td></tr><tr><td>Copy the app's exact visible wording</td><td>Paraphrasing is the most common cause of a tap landing on the wrong element</td></tr><tr><td>Disambiguate, don't describe</td><td>Drizz picks one element out of everything on screen. A label, a position in a list, a neighbor or a section narrows it to one</td></tr><tr><td>Add a wait after anything that navigates or calls the network</td><td>Validations retry a few times, which covers a slightly slow screen, not a four-second one</td></tr><tr><td>Set a variable before comparing it</td><td>A number hardcoded last quarter fails when the price changes</td></tr><tr><td>Validate directionally — <code>greater than</code>, <code>less than</code></td><td>Survives price and tax changes; an exact computed total does not</td></tr><tr><td>Validate after anything non-deterministic</td><td>Scroll stops silently at its attempt limit, so the validation is what catches the miss. Applies after <code>MAP_ACTION</code>, after a load-bearing <code>SET</code>, and after a scroll</td></tr><tr><td>Put anywhere-popups in <a href="/pages/wl2GqBAWrbDmrlNZGHGT">Memory</a></td><td>One rule replaces the same <code>IF</code> block in forty tests</td></tr><tr><td>Put flow-specific popups in <code>IF</code> blocks</td><td>An org-wide rule makes every test in the organization pay for a check only one flow needs</td></tr><tr><td>Put anything repeated into a <a href="/pages/92D9ohPqc6eyuPm4vkQJ">module</a></td><td>A login flow that changes twice a year exists in one place</td></tr><tr><td>Quote scroll targets and give a direction</td><td>An unquoted target with no direction doesn't resolve</td></tr><tr><td>Use <code>Tap</code> whenever the element has a visible label</td><td><code>MAP_ACTION</code> is the least deterministic command in Drizz</td></tr><tr><td>Validate a variable's <strong>value</strong> — <code>{{order_id}}</code></td><td><code>order_id</code> without braces is searched for as literal on-screen text</td></tr><tr><td>Store only what's rendered on screen</td><td>Vision AI reads pixels. System timestamps and internal IDs aren't visible to it</td></tr><tr><td>Reset state explicitly on iOS — log out, or reinstall</td><td><code>CLEAR_APP</code> is effectively a no-op on iOS</td></tr><tr><td>Use <code>Tap on the back button</code> for cross-platform back</td><td><code>PRESS_DEVICE_BACK_BUTTON</code> doesn't exist on iOS</td></tr><tr><td>Pick one style per value — <code>{{phone}}</code></td><td><code>&#x3C;phone></code> is inert text. Drizz types the literal characters</td></tr><tr><td>Bind a dataset to any plan whose tests use variables</td><td>A plan with variables and no test data bound can misread <code>SET</code> steps</td></tr><tr><td>Name variables for the moment — <code>total_before</code></td><td>Reusing a name overwrites the first capture permanently</td></tr><tr><td>Put environment and account values in a dataset</td><td>One script covers staging and production</td></tr><tr><td>Work the healed-steps list as a maintenance queue</td><td>A step that heals every run has a bad description</td></tr><tr><td>Delete tests that no longer mean anything</td><td>A green suite nobody trusts is worse than a smaller one people do</td></tr></tbody></table>

## Don't

<table data-search="false"><thead><tr><th>Rule</th><th>Why it matters</th></tr></thead><tbody><tr><td>Chain three scenarios into one long test</td><td>A failure doesn't say which feature broke</td></tr><tr><td>Paraphrase — "Proceed to Pay" for a button that says "Charge Now"</td><td>The tap lands on the wrong element or nothing at all</td></tr><tr><td>Scatter long waits everywhere as insurance</td><td>Hides real timing problems and slows the whole suite</td></tr><tr><td>Compare against a number hardcoded last quarter</td><td>Breaks on the next price or tax change</td></tr><tr><td>Compute an exact expected total by hand</td><td>Same failure, one release later</td></tr><tr><td>Copy the same <code>IF</code> popup block into forty tests</td><td>Forty edits when the popup changes</td></tr><tr><td>Add an org-wide blocker rule for a popup only you see</td><td>Every test in the organization pays for it</td></tr><tr><td>Copy-paste the login flow into every test</td><td>Every test changes when login changes</td></tr><tr><td>Write <code>Scroll until Proceed</code> with no quotes and no direction</td><td>The target doesn't resolve</td></tr><tr><td>Trust a grid-cell gesture without validating after it</td><td><code>MAP_ACTION</code> can hit the wrong thing and still report success</td></tr><tr><td>Reach for <code>MAP_ACTION</code> the first time a tap misses</td><td>Fix the description instead</td></tr><tr><td>Validate a variable's name — <code>order_id</code> as visible text</td><td>Drizz searches the screen for that literal string</td></tr><tr><td>Store a system timestamp or an internal ID</td><td>Not rendered, so nothing to read</td></tr><tr><td>Rely on <code>CLEAR_APP</code> for clean state on iOS</td><td>State carries over to the next test</td></tr><tr><td>Use <code>PRESS_DEVICE_BACK_BUTTON</code> in a script that runs on iOS</td><td>The command doesn't exist there</td></tr><tr><td>Mix <code>{{phone}}</code> and <code>&#x3C;phone></code> for the same value</td><td>One resolves, the other is typed literally</td></tr><tr><td>Run a plan with variables and no test data bound</td><td><code>SET</code> steps can be misread</td></tr><tr><td>Reuse one variable name for two captures</td><td>The second write destroys the first</td></tr></tbody></table>

## The shape that works

```
# T04 — ShopEase: search and add to cart

CALL shopease_login

Tap on the search icon
Wait Until 2 Seconds

Type running shoes in the search field
Tap on the first search result
Wait Until 3 Seconds

Validate that the product detail page is visible

Store the price as listed_price
Tap on Add to Cart
Wait Until 2 Seconds

Validate that Item added to cart is visible

CLEAR_APP com.shopease.android
```

Five beats, in order:

1. **Setup** — get to a known state, with a `CALL` to a shared login module.
2. **Navigate** — reach the screen under test.
3. **Act** — the scenario itself.
4. **Validate** — assert the outcome.
5. **Clean up** — `CLEAR_APP` so the next test starts fresh.

## Common mistakes

<table data-search="false"><thead><tr><th>What you write</th><th>What happens</th></tr></thead><tbody><tr><td><code>Tap on the button</code></td><td>Ambiguous. Resolves differently between runs</td></tr><tr><td><code>Wait until the page loads</code></td><td>Not a fixed duration. Write <code>Wait Until 5 Seconds</code></td></tr><tr><td><code>Scroll until Proceed</code></td><td>No quotes, no direction. Quote the target and say <code>down</code></td></tr><tr><td>One test covering login, search, cart and checkout</td><td>When it fails you don't know which feature broke</td></tr><tr><td><code>CLEAR_APP</code> as your iOS teardown</td><td>Does effectively nothing on iOS. State carries over</td></tr><tr><td><code>PRESS_DEVICE_BACK_BUTTON</code> in a cross-platform test</td><td>Doesn't exist on iOS</td></tr><tr><td>A 20-second wait to fix a flaky step</td><td>Hides a real timing problem and slows the whole suite</td></tr><tr><td><code>MAP_ACTION</code> on an element that has a label</td><td>The least deterministic command in Drizz, used for no reason</td></tr></tbody></table>

## Next

* [The shape of a test](/writing-tests/writing-tests)
* [Recipes](/writing-tests/recipes)
* [Known limitations](/reference/known-limitations)

***

*Last updated: 6 August 2026*


# Recipes

Six complete, runnable Drizz tests — login, checkout, multi-app, mobile web, location and data-driven runs.

Six complete tests against ShopEase, the demo app used throughout these docs. Each runs as written.

|                  |                                                                       |
| ---------------- | --------------------------------------------------------------------- |
| **Platforms**    | Android · iOS— except where a recipe says otherwise                   |
| **Demo app**     | ShopEase — `com.shopease.android` / `com.shopease.ios`                |
| **Test user**    | `qa@example.com` · phone `9000000000` · OTP `123456`                  |
| **Every recipe** | Ends in `CLEAR_APP` so the next test starts clean                     |
| **Watch out**    | `CLEAR_APP` is effectively a no-op on iOS. Log out explicitly instead |

## Prerequisites

* Drizz desktop app installed and signed in
* ShopEase registered as an app, with a device or emulator connected
* A dataset supplying `app_package`, `phone` and `otp`, bound to the plan

## 1 · Login with OTP

The setup module every other recipe calls, driven by dataset variables.

```
# BEGIN shopease_login
OPEN_APP {{app_package}}
Wait Until 5 Seconds

IF Validate that the location permission prompt is visible
{
    Tap on While using the app
    Wait Until 1 Seconds
}

Type {{phone}} in the mobile number field
Tap on Get OTP
Wait Until 3 Seconds

Type {{otp}} in the OTP field
Tap on Continue
Wait Until 5 Seconds

Validate that the home screen is visible
# END shopease_login
```

The test that proves it works:

```
# T01 — ShopEase: login with OTP

CALL shopease_login

Validate 1. Search 2. Cart 3. Account is visible

CLEAR_APP com.shopease.android
```

An app that returns the OTP on screen rather than by SMS supports capturing it instead of hardcoding it:

```
Store the OTP as otp_initial
Type {{otp_initial}} in the OTP field
```

## 2 · Search, add to cart, check out

The five-beat test — setup, navigate, act, validate, clean up — with a before/after numeric comparison.

```
# T02 — ShopEase: search, add to cart and place an order

CALL shopease_login

Tap on the search icon
Wait Until 2 Seconds

Type running shoes in the search field
Tap on the first search result
Wait Until 3 Seconds

Validate that the product detail page is visible
Store the price as listed_price

Tap on Add to Cart
Wait Until 2 Seconds
Validate that Item added to cart is visible

Tap on Cart
Wait Until 2 Seconds

Validate that {{listed_price}} is visible beside the item
Store the Order Total as total_before

# the checkout button sits below the fold on smaller screens;
# a scroll that isn't needed costs almost nothing
Scroll down until "Proceed to Checkout" is visible
Tap on Proceed to Checkout
Wait Until 3 Seconds

Validate that the delivery address is visible
Tap on Place Order
Wait Until 5 Seconds

Validate 1. Order placed 2. Order ID 3. Estimated delivery is visible
Store the Order ID as order_id

CLEAR_APP com.shopease.android
```

## 3 · Multi-app: place an order, accept it in the partner app

Switching between two apps in one test with `OPEN_APP`, carrying a value across the switch.

```
# T03 — ShopEase: an order placed in the customer app appears in the partner app

CALL shopease_login

Tap on the search icon
Type running shoes in the search field
Tap on the first search result
Wait Until 3 Seconds

Tap on Add to Cart
Wait Until 2 Seconds

Tap on Cart
Scroll down until "Proceed to Checkout" is visible
Tap on Proceed to Checkout
Wait Until 3 Seconds

Tap on Place Order
Wait Until 5 Seconds

Validate that Order placed is visible
Store the Order ID as order_id

# switch to the delivery partner app
OPEN_APP com.shopease.partner
Wait Until 5 Seconds

Tap on Available Orders
Wait Until 3 Seconds

Scroll down until "{{order_id}}" is visible
Validate that {{order_id}} is visible in the available orders list

Tap on Accept beside {{order_id}}
Wait Until 3 Seconds

Validate that Order accepted is visible

KILL_APP com.shopease.partner
CLEAR_APP com.shopease.android
```

`OPEN_APP` switches apps directly. `MINIMISE_APP` followed by a tap on the app from the home screen is the slower alternative, closer to what a person does. Variables survive the switch — `{{order_id}}` captured in ShopEase is still set in the partner app.

## 4 · Mobile web: check the order on the website

Validating the same data in an app and in a browser, in one test.

```
# T04 — ShopEase: the order in the app matches the order on the website

CALL shopease_login

Tap on Account
Wait Until 2 Seconds

Tap on My Orders
Wait Until 3 Seconds

Tap on the first order
Wait Until 2 Seconds

Validate that the order detail page is visible
Store the Order ID as order_id
Store the Order Total as order_total

# switch to the browser
OPEN_APP com.android.chrome
Wait Until 5 Seconds

Tap on the address bar
Type shopease.example.com/orders in the address bar
Wait Until 5 Seconds

Type {{order_id}} in the order search field
Tap on Search
Wait Until 5 Seconds

Validate that {{order_id}} is visible on the page
Validate that {{order_total}} is visible on the page

KILL_APP com.android.chrome
CLEAR_APP com.shopease.android
```

The browser is another app. `Tap`, `Type` and `Validate` work the same on a web page.

## 5 · Location: offers change with the city

Mocking GPS, and the kill-and-relaunch that makes the app pick it up. Android only — `SET_GPS` mocks the device location, and the app has to be restarted to read it.

```
# T05 — ShopEase: the offers screen follows the mocked location

# Bangalore
SET_GPS(latitude=12.9716, longitude=77.5946)

CALL shopease_login

Tap on Offers
Wait Until 3 Seconds

Validate that Bangalore is visible on the offers screen
Store the first offer title as bangalore_offer

# move to Delhi — the order KILL_APP, SET_GPS, OPEN_APP is required
KILL_APP com.shopease.android
SET_GPS(latitude=28.7041, longitude=77.1025)

OPEN_APP com.shopease.android
Wait Until 5 Seconds

Tap on Offers
Wait Until 3 Seconds

Validate that Delhi is visible on the offers screen
Store the first offer title as delhi_offer

Validate that delhi_offer is not equal to bangalore_offer

CLEAR_APP com.shopease.android
```

An app already running keeps the location it started with. `SET_GPS` without a restart changes nothing and the test passes for the wrong reason. A test that needs one location throughout sets it before `OPEN_APP` and skips the restart.

## 6 · Data-driven: one test, every environment and account

A single script driven entirely by a dataset, so it runs against staging, production, or ten different users without editing.

The dataset, `shopease_staging`:

```yaml
app_package: com.shopease.android
phone: "9000000000"
otp: "123456"
search_term: running shoes
expected_city: Bangalore
```

The test:

```
# T06 — ShopEase: search and add to cart, fully data-driven

CALL shopease_login

Validate that {{expected_city}} is visible on the address bar

Tap on the search icon
Wait Until 2 Seconds

Type {{search_term}} in the search field
Wait Until 3 Seconds

Validate that {{search_term}} is visible in the search field
Tap on the first search result
Wait Until 3 Seconds

Validate that the product detail page is visible
Tap on Add to Cart
Wait Until 2 Seconds

Tap on Cart
Wait Until 2 Seconds
Validate that the cart has 1 item

CLEAR_APP {{app_package}}
```

Nothing in the script names an environment. A second dataset, `shopease_production`, with the production package and a production account, bound to a second plan, covers production from the same file.

Accounts that can't be shared between parallel runs go in a [lockable pool](/writing-tests/which-variable/lockable-pools), so each run leases its own row.

## Common mistakes

<table data-search="false"><thead><tr><th>What you write</th><th>What happens</th></tr></thead><tbody><tr><td><code>SET_GPS</code> without killing and relaunching the app</td><td>The app keeps the location it started with. The test passes for the wrong reason</td></tr><tr><td><code>OPEN_APP</code> with no package argument</td><td>Fails by design. The package is required</td></tr><tr><td>Browser steps with app-length waits</td><td>The page hasn't loaded and the validation fails</td></tr><tr><td>A hardcoded package in a data-driven test</td><td>The one line you have to edit per environment, which someone will forget</td></tr><tr><td>Chaining all six recipes into one test</td><td>When it fails you don't know which feature broke</td></tr><tr><td><code>CLEAR_APP</code> as teardown on iOS</td><td>Does effectively nothing. Log out explicitly instead</td></tr><tr><td>Reusing one variable name across an app switch</td><td>The second capture overwrites the first, and the comparison is meaningless</td></tr></tbody></table>

## Next

* [Authoring rules](/writing-tests/authoring-rules)
* [Datasets & test data](/writing-tests/which-variable/datasets)
* [Test plans](/running-tests/test-plans)

***

*Last updated: 6 August 2026*


# Running locally

Run a test on the device connected to your Mac, watch each step execute, and stop the run.

A local run executes a test on the device connected to your Mac, with live screenshots in the console.

|                   |                                                                               |
| ----------------- | ----------------------------------------------------------------------------- |
| **Platforms**     | Android · iOS                                                                 |
| **Where it runs** | Your Mac, against one connected device                                        |
| **Parallelism**   | One test at a time — use a [test plan](/running-tests/test-plans) to run many |
| **Queues**        | None. A local run starts immediately                                          |
| **Watch out**     | Local runs are metered the same way as cloud runs                             |

## Prerequisites

* Drizz desktop app installed and signed in
* One emulator, simulator or USB-connected device
* A physical Android device unlocked — Drizz refuses a locked device
* The app under test available on the device or in the app list

## Copy this

A complete test against ShopEase, the demo app used throughout these docs. Swap the package name and the screen labels for your app's.

```
# ShopEase — search for a product and open it

OPEN_APP com.shopease.android
Wait Until 5 Seconds

Validate that the home screen is visible

Tap on the search icon
Wait Until 2 Seconds

Type running shoes in the search field
Tap on the first search result
Wait Until 3 Seconds

Validate that the product detail page is visible

CLEAR_APP com.shopease.android
```

## Run a test

1. Open the Connect Device screen.
2. Select your emulator, simulator or attached device.
3. Confirm the device shows as connected. Connect once per session.
4. Select the app under test from the app list. Drizz installs and launches it if it isn't already present.
5. Click run — for the open file in the editor, or for the lines you have selected.
6. Confirm the first step reports a status decorator beside its line in the editor.

## Platform differences

{% tabs %}
{% tab title="Android" %}

* Connect an emulator or a USB device with **Developer options** and **USB debugging** enabled.
* `CLEAR_APP com.shopease.android` wipes app data between runs.
* Disable the emulator soft keyboard. It covers elements and the tap after a `Type` fails as undoable.
  {% endtab %}

{% tab title="iOS" %}

* Connect a simulator, or a physical device trusted by the Mac.
* `CLEAR_APP` has no effect on iOS. Log out in-app, or reinstall between tests.
* iOS steps run roughly twice as slow as the equivalent Android steps.
  {% endtab %}
  {% endtabs %}

## What the console shows

| Element                            | Content                                                     |
| ---------------------------------- | ----------------------------------------------------------- |
| **Live execution step level**      | The device screen at the moment the step ran                |
| **Step classification**            | How the line was read — tap, type, validate, scroll, system |
| **Explainable reasoning per step** | Why Drizz selected the element it selected                  |
| **Status decorator in the editor** | Pending, running, passed or failed, beside the line         |
| **Timestamps**                     | Per-step duration                                           |
| **Token usage in the run banner**  | DT spent so far on this run                                 |

Steps that resolve from cache appear as *"Predicted Action drizzing fast ⚡"* and finish in a fraction of the time — see [Caching](/running-tests/caching). Cached steps are the cheapest step category; Vision AI steps are the most expensive.

When the run ends Drizz writes a report: a run summary, per-step outcomes with before and after screenshots, the logs, and a failure summary. See [Reading a report](/reports-and-debugging/reading-a-report).

## Stop a run

1. Click stop.
2. Wait for the step in flight to finish. Drizz releases the device and writes a report for the steps that ran.

{% hint style="success" %}
If the UI sits on **"Stopping"**, wait. Drizz has a safety-net timeout that recovers a stuck stop. If it hasn't cleared after that, restart the desktop app — see [Troubleshooting](/reports-and-debugging/troubleshooting).
{% endhint %}

## Local run limits

A local run cannot do any of the following. Use a [test plan](/running-tests/test-plans) instead.

| Limit                   | Detail                                   |
| ----------------------- | ---------------------------------------- |
| **One test per run**    | Multiple tests in one run require a plan |
| **No parallelism**      | One test at a time                       |
| **Local devices only**  | Cloud and private devices are plan-only  |
| **Manual trigger only** | CI triggers a plan, not a local run      |
| **No quality gate**     | A pass/fail threshold is a plan setting  |

## Common mistakes

| What you do                                | What happens                                                    |
| ------------------------------------------ | --------------------------------------------------------------- |
| Leave the emulator soft keyboard enabled   | It covers elements and the tap after a `Type` fails as undoable |
| Judge suite speed on the first run         | The cache is cold. Runs two and three are the honest measure    |
| Rely on `CLEAR_APP` for clean state on iOS | It's a no-op there — the next run reuses state                  |
| Force-quit the app on a stuck "Stopping"   | The timeout recovers it, and you lose the partial report        |
| Lock the phone mid-run                     | The run fails on the next step. Keep it awake and unlocked      |

## Next

* [Test plans](/running-tests/test-plans)
* [Reading a report](/reports-and-debugging/reading-a-report)
* [When a step fails](/reports-and-debugging/when-a-step-fails)

***

*Last updated: 6 August 2026*


# Test plans

A test plan is a set of tests plus how to run them — devices, app build, dataset, concurrency and a quality gate.

A test plan is a set of tests plus the settings that run them: app build, devices, dataset, concurrency and a quality gate.

|                    |                                                                                           |
| ------------------ | ----------------------------------------------------------------------------------------- |
| **Platforms**      | Android · iOS (real-device build required)                                                |
| **A plan holds**   | Test cases · execution settings · reports                                                 |
| **App upload cap** | 500 MB per build                                                                          |
| **Runs on**        | Device cloud                                                                              |
| **Watch out**      | Concurrency is capped by available devices *and* by lockable pool size — the smaller wins |

## Prerequisites

* A registered app build — an APK, or an iOS real-device build, under 500 MB
* At least one saved test
* A dataset, if any test in the plan uses `{{variables}}`
* A lockable pool sized to your target concurrency, if any test leases from one

## What a plan holds

Three parts. Each changes independently of the others.

| Part                   | Contents                                                        | Changing it                                                     |
| ---------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- |
| **Test cases**         | Which tests run, and in what order                              | Add, remove or reorder. The underlying test files are untouched |
| **Execution settings** | App build, devices, dataset, concurrency, quality gate          | Change per run without editing a test                           |
| **Reports**            | Results, run history and recordings, aggregated across the plan | Written automatically after every run                           |

## Plan settings

| Setting          | What it controls                                          | Constraints                                                              |
| ---------------- | --------------------------------------------------------- | ------------------------------------------------------------------------ |
| **Test cases**   | Which tests run, in which order                           | Order is execution order only; it never edits a test file                |
| **Devices**      | Local, cloud or private, and the model and OS version     | See [Devices](/running-tests/devices). If nothing is free, the run waits |
| **Concurrency**  | How many tests run at once                                | Capped by available devices and by lockable pool size — the smaller wins |
| **Dataset**      | The values every `{{variable}}` in the plan resolves from | Bind at plan level, not in the script. Bind one even if empty            |
| **Quality gate** | The pass threshold the plan must clear to count as passed | A percentage, checked by CI                                              |
| **App build**    | The registered build the plan installs                    | 500 MB cap. iOS requires a real-device build                             |

## Plan-only capabilities

| Capability                                               | Available locally |
| -------------------------------------------------------- | ----------------- |
| Several tests in one run                                 | ❌                 |
| Parallel execution                                       | ❌                 |
| Cloud and private devices                                | ❌                 |
| CI-triggered runs                                        | ❌                 |
| Dataset-driven runs across many accounts or environments | ❌                 |
| Quality gate on the run result                           | ❌                 |

## App builds

Plans run against a **registered app** — a build uploaded to Drizz. Local runs use whatever is installed on the device; plans do not.

|                                  |                                                           |
| -------------------------------- | --------------------------------------------------------- |
| **Maximum upload size**          | 500 MB                                                    |
| **Android**                      | An APK                                                    |
| **iOS**                          | A **real-device build**.                                  |
| **Downloading a build back out** | Not supported — keep your own copy of anything you upload |

## Devices

A plan runs on local, cloud or private devices. See [Devices](/running-tests/devices) for the comparison and for what happens when nothing is free.

## Data

If tests use `{{variables}}`, bind a **dataset** to the plan. Binding happens at plan level, not in the script, so one suite runs against staging and production without an edit.

## Concurrency and quality gates

**Concurrency** is how many tests in the plan run at once. Two ceilings apply on top of the number you set:

* The **devices available** to you at that moment.
* Any **lockable pool** your tests lease from. A 4-row pool means at most 4 parallel runs, whatever concurrency is configured.

**Quality gate** is the threshold a plan must clear to count as passed — for example, pass if 90% or more of tests pass. That is the number a CI job checks. At 100%, a single flaky test blocks every build.

Both are set on the run screen — see [Creating & running a plan](/running-tests/creating-and-running-a-plan).

## Common mistakes

<table data-search="false"><thead><tr><th>What you do</th><th>What happens</th></tr></thead><tbody><tr><td>Bundle every flow into one plan</td><td>One failure buries the signal, and a re-run costs the whole suite</td></tr><tr><td>Name plans <code>Plan 1</code>, <code>Plan 2</code></td><td>A failed run doesn't say what it covered. Use <code>Checkout_Regression</code>, <code>Login_Sanity</code></td></tr><tr><td>Set concurrency to 20 with a 4-row lockable pool</td><td>16 runs sit waiting. The pool is the real limit</td></tr><tr><td>Upload an iOS simulator build</td><td>Rejected at plan creation. Plans need a real-device build</td></tr><tr><td>Leave variables in tests with no dataset bound</td><td><code>SET</code> misclassifies and the run goes wrong quietly</td></tr><tr><td>Mix device models within a plan</td><td>Device-specific failures read as flakiness</td></tr><tr><td>Skip the healed-step list</td><td>A step that heals repeatedly has a weak description — see <a href="/pages/WhpvSc0GZjCheriEDXSF">Self-healing</a></td></tr></tbody></table>

## Next

* [Creating & running a plan](/running-tests/creating-and-running-a-plan)
* [Devices](/running-tests/devices)
* [Reading a report](/reports-and-debugging/reading-a-report)

***

*Last updated: 6 August 2026*


# Creating & Running a plan

Create a test plan, add tests, pick devices and data, set concurrency and a quality gate, run it, and read the run screen.

Eight steps create a plan. One button runs it. The run screen reports progress while it runs.

|                         |                                                                               |
| ----------------------- | ----------------------------------------------------------------------------- |
| **Platforms**           | Android · iOS (real-device build required)                                    |
| **You need first**      | A registered app build, and at least one saved test                           |
| **Set per plan**        | Devices · concurrency · dataset · quality gate                                |
| **Concurrency ceiling** | The lower of available devices and lockable pool size                         |
| **Watch out**           | Reordering tests changes execution order only — it never edits the test files |

## Prerequisites

* Access to Drizz Cloud
* A registered app build — APK, or iOS real-device build, under 500 MB
* At least one saved test in the project
* A dataset, if any test uses `{{variables}}`

## Create the plan

1. Open the **Test Plans** section in Drizz Cloud and click **Create Test Plan**.
2. Enter a name, an optional description, and the project. Name it for what it covers — `Checkout_Regression`, `Login_Sanity`. This name appears in every report and every CI log.
3. Select the provider: emulator, simulator or real device. See [Devices](/running-tests/devices).
4. Select the registered app build. iOS plans require a real-device build; a simulator build is rejected here. Uploads cap at 500 MB.
5. Select the device configuration: model, OS version and device type. Hold this configuration steady across runs of the same plan.
6. Select the test cases from your saved tests.
7. Drag the test cases into execution order. This sets run order only; it does not modify the test files.
8. Click **Save**.
9. Confirm the plan appears in the Test Plans list with a run control. The plan stays editable — adding, removing or reordering tests and changing devices does not discard run history.

## Configure the run

Four settings, all stored on the plan rather than in the scripts.

| Setting             | What it does                                                                                                                                       |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Devices**         | How many devices the plan can use, and of what kind. If nothing is free, the run waits rather than failing — see [Devices](/running-tests/devices) |
| **Concurrency**     | How many tests run at once                                                                                                                         |
| **Dataset binding** | The values every `{{variable}}` in every test in the plan resolves from                                                                            |
| **Quality gate**    | The pass threshold the plan must clear to count as passed                                                                                          |

### Concurrency ceilings

Two limits apply on top of the number you set.

| Cap                          | Effect                                                                         |
| ---------------------------- | ------------------------------------------------------------------------------ |
| **Devices available to you** | 6 free devices means 6 tests at once, whatever you set                         |
| **Lockable pool size**       | A 4-row pool means at most 4 parallel runs — each run leases a row exclusively |

A suite that runs 10-wide needs 10 or more rows in the pool — see [Lockable pools](/writing-tests/which-variable/lockable-pools).

### Dataset binding

Bind a dataset if any test in the plan uses `{{variables}}`. Every `{{variable}}` in every test in the plan resolves from it, so the same suite runs against staging and production with no script change.

{% hint style="warning" %}
Bind a dataset to any plan containing variables, **even an empty one**. A plan with variables and no dataset bound misclassifies `SET` commands and the run goes wrong quietly.&#x20;
{% endhint %}

### Quality gate

The threshold the plan must clear to count as passed — for example, pass if 90% or more of tests pass. This is the number a CI job gates on. At 100%, one flaky test blocks every build.

## Run the plan

{% tabs %}
{% tab title="From the dashboard" %}

1. Open the plan.
2. Click **Run**.
3. Confirm the run screen opens and reports allocated devices.
   {% endtab %}

{% tab title="From CI" %}

1. Authenticate against the API — see [Authenticate](/automate-and-integrate/authenticate).
2. Upload the build — see [Upload a build](/automate-and-integrate/upload-a-build).
3. Trigger the plan — see [Trigger a run](/automate-and-integrate/trigger-a-run).
4. Confirm the trigger response returns a run identifier.

There is no run-status endpoint. CI can start a plan but cannot wait on it or gate a build on the outcome — see [CI/CD](/automate-and-integrate/ci-cd).
{% endtab %}
{% endtabs %}

Drizz allocates devices, installs the build, and starts tests up to the concurrency limit. Runs that can't get a device wait in the queue.

## Watch the run

| Area                | Contents                                                                                                      |
| ------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Summary**         | Total tests, passed, failed, errored, and completion status                                                   |
| **Thread view**     | One lane per parallel thread, each with its own identifier, for following a single test through a 10-wide run |
| **Per-test detail** | Status, healed-step count, elapsed time, start and finish timestamps, and the device it ran on                |
| **Artifacts**       | Screenshots, step logs and the recording, per test attempt                                                    |
| **Run history**     | The same plan's previous runs                                                                                 |

Expand a test to see its step list. Expand a step to see its before and after screenshots and reasoning — see [When a step fails](/reports-and-debugging/when-a-step-fails).

## Edit a plan after it has run

None of these modify authored tests, and none discard run history:

* Add or remove test cases.
* Reorder test cases.
* Change devices, OS versions or the provider.
* Swap the dataset.
* Point the plan at a new build.

## Common mistakes

| What you do                                                             | What happens                                                             |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Set concurrency above your pool size                                    | Extra runs wait on a lease, and the suite takes longer                   |
| Set the quality gate to 100%                                            | One flaky test blocks every build                                        |
| Change the device model between runs of the same plan                   | Real regressions read as flakiness                                       |
| Reorder tests to fix a failure                                          | If test B needs test A's state, make it explicit in the script           |
| Run the plan without rebinding the dataset after switching environments | The suite runs green against the wrong backend                           |
| Trigger from CI and assume it gates the build                           | There's no run-status endpoint. CI can start a plan but can't wait on it |

## Next

* [Devices](/running-tests/devices)
* [Reading a report](/reports-and-debugging/reading-a-report)
* [Statuses](/reports-and-debugging/statuses)

***

*Last updated: 6 August 2026*


# Devices

The three places a Drizz test runs — your Mac, the Drizz device cloud, or your org's private devices — and what happens when none is free.

A test runs in one of three places: on your own Mac, on the Drizz device cloud, or on devices reserved for your organization.

|                       |                                                                     |
| --------------------- | ------------------------------------------------------------------- |
| **Platforms**         | Android · iOS                                                       |
| **Three types**       | Local · Cloud · Private                                             |
| **Session isolation** | Every test gets its own device session                              |
| **No device free**    | The run waits — it doesn't fail. Wait reason `pool_exhausted`       |
| **Watch out**         | Local runs one test at a time. Parallelism only exists in the cloud |

## Prerequisites

* Drizz desktop app installed and signed in, for local devices
* A test plan, for cloud and private devices
* Private devices arranged with your Drizz contact

## Device types

<table data-header-hidden data-search="false"><thead><tr><th></th><th></th><th></th><th></th></tr></thead><tbody><tr><td></td><td><strong>Local</strong></td><td><strong>Cloud</strong></td><td><strong>Private</strong></td></tr><tr><td><strong>Where</strong></td><td>Your Mac — emulator, simulator, or a USB device</td><td>The Drizz device cloud</td><td>Your own devices, reserved for your org</td></tr><tr><td><strong>Applies to</strong></td><td>Authoring and debugging</td><td>Regression, parallel runs, device coverage</td><td>Compliance, specific hardware, SIM and carrier testing</td></tr><tr><td><strong>Parallelism</strong></td><td>One test at a time</td><td>Many tests at once</td><td>Bounded by device count</td></tr><tr><td><strong>Queuing</strong></td><td>None — starts immediately</td><td>Waits when nothing is free</td><td>Waits when nothing is free</td></tr><tr><td><strong>Device range</strong></td><td>Whatever you have</td><td>A catalogue of models and OS versions</td><td>Exactly your hardware</td></tr><tr><td><strong>App install</strong></td><td>Whatever is on the device</td><td>The registered build the plan points at</td><td>The registered build the plan points at</td></tr><tr><td><strong>Set up by</strong></td><td>You, once, via the guided wizard</td><td>Nothing to set up</td><td>Arranged with your Drizz contact</td></tr></tbody></table>

## Getting a device

{% tabs %}
{% tab title="Local" %}

1. Open the guided wizard in the desktop app — see [Set up a device: guided wizard](/desktop-app/device-setup-wizard).
2. Complete setup for your emulator, simulator or USB device.
3. Open the Connect Device screen and select the device.
4. Confirm it shows as connected.

Local devices run one test at a time and never queue.
{% endtab %}

{% tab title="Cloud Public and Private Devices" %}

1. Open Integrations.
2. Authorise.
3. List of devices and OS will be shown which creating testplan.
   {% endtab %}
   {% endtabs %}

## Sessions are isolated

Every test gets its own device session. A 10-wide run is ten separate sessions, not ten tests sharing a device.

* Parallel tests cannot contaminate each other. One test's login state, cache or leftover data is invisible to the others.
* Test order within a plan is about dependency, not cleanup.
* State carried between tests must be explicit — a shared login module, or a dataset value.

## Waiting, and `pool_exhausted`

When nothing is free, a run **waits rather than failing**, and reports a wait reason.

| Wait reason               | Meaning                                                                                                         |
| ------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `pool_exhausted`          | No device is free, or every row of a lockable pool is leased                                                    |
| Waiting with devices idle | The block is data, not hardware — check your [lockable pool](/writing-tests/which-variable/lockable-pools) size |

A wait times out. Runs that repeatedly wait have one of three causes: insufficient device capacity, concurrency set above capacity, or a lockable pool smaller than concurrency.

## Limits

| Limit                           | Detail                                                                                |
| ------------------------------- | ------------------------------------------------------------------------------------- |
| **Location**                    | Set when a cloud device is provisioned. Cannot be changed mid-test                    |
| **Precise location permission** | Not grantable on every device — see [Known limitations](/reference/known-limitations) |

## Common mistakes

| What you do                                       | What happens                                                                   |
| ------------------------------------------------- | ------------------------------------------------------------------------------ |
| Raise concurrency to clear a queue                | If the block is a lockable pool, more concurrency adds waiting, not throughput |
| Assume a run used the device you asked for        | It may have fallen back. Read the device recorded on the run                   |
| Debug a failing test on cloud devices             | Slower, and you can't watch the screen. Reproduce locally                      |
| Rely on one test cleaning up for the next         | Sessions are isolated. Make shared state explicit                              |
| Treat a waiting run as a failure                  | It's queued. It will start or time out                                         |
| Compare pass rates across different device models | Device-specific failures read as flakiness                                     |

## Next

* [Creating & running a plan](/running-tests/creating-and-running-a-plan)
* [Lockable pools](/writing-tests/which-variable/lockable-pools)
* [Troubleshooting](/reports-and-debugging/troubleshooting)

***

*Last updated: 6 August 2026*


# Self-healing

When a step fails, Drizz repairs it mid-run instead of failing the whole test. Healed steps are badged in the report.

When a step fails, Drizz attempts to repair it mid-run and continue. Every repaired step is badged in the report.

|                   |                                                                |
| ----------------- | -------------------------------------------------------------- |
| **Platforms**     | Android · iOS                                                  |
| **Handles**       | Tap · Type · Swipe · Validate                                  |
| **Attempts**      | Around 5 per run. After that, failures fail                    |
| **In the report** | Healed steps are badged, and the run reads **Passed (healed)** |
| **Watch out**     | Healing attempts are shared across the whole run, not per step |

## Prerequisites

None. Healing is on by default and requires no configuration.

## Behavior

<table data-search="false"><thead><tr><th></th><th></th></tr></thead><tbody><tr><td><strong>Trigger</strong></td><td>A step fails</td></tr><tr><td><strong>Action</strong></td><td>Drizz reads the current screen, resolves the intent of the step, and attempts an alternative route to it</td></tr><tr><td><strong>On success</strong></td><td>The run continues from that step</td></tr><tr><td><strong>Step types handled</strong></td><td>Tap, type and swipe. All other failures fail normally</td></tr><tr><td><strong>Attempt budget</strong></td><td>Around 5 per run, shared across every step in the run. Once spent, later failures fail outright even if repairable</td></tr><tr><td><strong>Report marking</strong></td><td>Healed steps carry a healed badge</td></tr><tr><td><strong>Run status</strong></td><td><code>Passed (healed)</code>, not <code>Passed</code> — see <a href="/pages/75PJpg9l527HNAFO85yu">Statuses</a></td></tr><tr><td><strong>Cannot resume</strong></td><td>If Drizz cannot rejoin the original script after a repair, the run reports that it could not resume. This is a failure, not a healed pass</td></tr></tbody></table>

A step that heals on repeated runs has a weak description. Healed steps are badged in the report so they can be located and rewritten — use the app's exact on-screen wording, or add neighbor or section context. See [When a step fails](/reports-and-debugging/when-a-step-fails).

{% hint style="warning" %}
Healing attempts are shared across the whole run. Steps healing early in a test spend the budget a transient failure later in the same test would have used.
{% endhint %}

## Turn healing off

Healing is disabled per app by Drizz. There is no self-serve setting.

1. Identify the package or bundle ID.
2. Ask your Drizz contact to disable healing for it.
3. Run the suite and confirm failed steps now fail outright rather than reporting `Passed (healed)`.

## Common mistakes

| What you do                                               | What happens                                                           |
| --------------------------------------------------------- | ---------------------------------------------------------------------- |
| Read `Passed (healed)` as `Passed`                        | The suite is held together by repairs and the pass rate hides it       |
| Skip the healed list on a green run                       | The same steps fail outright on a later run                            |
| Add waits to stop a step healing                          | Healing responds to the description, not the timing                    |
| Assume healing rescues any failure                        | It handles tap, type and swipe only, and only while attempts remain    |
| Expect healing to fix a wrong-screen failure              | If an earlier step went wrong, the repair belongs on that earlier step |
| Leave a paraphrased description in place because it heals | Every run spends attempt budget on it                                  |

## Next

* [When a step fails](/reports-and-debugging/when-a-step-fails)
* [Statuses](/reports-and-debugging/statuses)
* [Reading a report](/reports-and-debugging/reading-a-report)

***

*Last updated: 6 August 2026*


# Caching

Drizz reuses how it resolved a step on a screen it has seen before. Cached steps run faster and cost less.

Drizz reuses how it resolved a step on a screen it has seen before, instead of resolving it again. Cached steps run in a fraction of the time and are the cheapest step category.

|                |                                                                      |
| -------------- | -------------------------------------------------------------------- |
| **Platforms**  | Android · iOS                                                        |
| **In the log** | *"Predicted Action drizzing fast ⚡"*                                 |
| **Speed**      | A fraction of the time of an uncached step                           |
| **Cost**       | Cached steps are the cheapest category; Vision AI the most expensive |
| **Watch out**  | A UI change invalidates the cache for the screens it touched         |

## Prerequisites

None. Caching is enabled per app by Drizz and requires no configuration in a test.

## Behavior

<table data-search="false"><thead><tr><th>Condition</th><th>Behavior</th></tr></thead><tbody><tr><td><strong>Step resolves from cache</strong></td><td>Logged as <em>"Predicted Action drizzing fast ⚡"</em>, completes in a fraction of a second</td></tr><tr><td><strong>Step does not resolve from cache</strong></td><td>Logged with its normal step classification, completes in seconds</td></tr><tr><td><strong>Cost, cached step</strong></td><td>Cheapest step category</td></tr><tr><td><strong>Cost, Vision AI step</strong></td><td>Most expensive step category — see <a href="/pages/wO93UDumZZuCQ72kJzR0">Billing &#x26; tokens</a></td></tr><tr><td><strong>First run of a new test</strong></td><td>No cache. Every step does full resolution</td></tr><tr><td><strong>Second and later runs</strong></td><td>A growing share of steps resolve from cache</td></tr><tr><td><strong>After a UI change</strong></td><td>The cache for the affected screens is invalidated. Those steps run uncached until it rebuilds over subsequent runs</td></tr><tr><td><strong>Stale resolution after a UI change</strong></td><td>A cached step can act on an element as it was on the previous version of the screen</td></tr><tr><td><strong>Enabling</strong></td><td>Per app, by Drizz. Steps that never resolve from cache after repeated runs indicate caching is not on for that package</td></tr><tr><td><strong>Configuration in a test</strong></td><td>None. Caching is not written into or enabled per test</td></tr></tbody></table>

### If a cached step acts on stale UI

1. Re-run the test. The cache updates as the suite runs against the new UI.
2. Re-run once more.
3. If the step is still wrong on the third run, fix the description — see [When a step fails](/reports-and-debugging/when-a-step-fails).

## Common mistakes

| What you do                                                 | What happens                                                                  |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Benchmark a new suite on its first run                      | You measure the cold path                                                     |
| Estimate cost from the first run's usage                    | Cost drops once the suite is warm and steps resolve from cache                |
| Raise a support ticket when the suite slows after a release | Expected. The UI changed and the cache is rebuilding                          |
| Rewrite descriptions to force a cache hit                   | Caching follows description stability. Write descriptions for clarity         |
| Assume a stale cache when a step fails                      | Check the before-screenshot first. The description is the more frequent cause |
| Run the suite only before a release                         | Fewer runs, less of the suite cached                                          |

## Next

* [Billing & tokens](/your-account/billing-and-tokens)
* [When a step fails](/reports-and-debugging/when-a-step-fails)
* [Running locally](/running-tests/running-locally)

***

*Last updated: 6 August 2026*


# Game mode

Games don't expose a normal view hierarchy, so Drizz can run vision-only. Game mode is enabled per app by Drizz.

Game mode runs Drizz from the rendered screen alone, ignoring the view hierarchy.

|                     |                                                                |
| ------------------- | -------------------------------------------------------------- |
| **Platforms**       | Android · iOS                                                  |
| **What it changes** | Drizz works from the screen alone, ignoring the view hierarchy |
| **Who turns it on** | Drizz, per app — not self-serve                                |
| **How to get it**   | Ask your Drizz contact                                         |

## Prerequisites

* A package or bundle ID to enable it against
* A Drizz contact — there is no self-serve setting

## What it applies to

A normal app exposes a view hierarchy Drizz reads alongside the screenshot. A game rendered on a canvas or in an engine exposes one large surface with nothing named inside it. Game mode drops the hierarchy and works from the rendered screen.

It applies to games, and to any app with a screen that renders as a single canvas — a map, a chart surface, or an embedded engine — where normal targeting fails on that screen.

## Get it enabled

1. Identify the package or bundle ID.
2. Ask your Drizz contact to enable game mode for it.
3. Run a test against the affected screen and confirm targeting resolves.

## Common mistakes

| What you do                                            | What happens                                                                             |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| Look for a game mode toggle in the app                 | There isn't one. It's enabled per app by Drizz                                           |
| Assume normal command behavior carries over unchanged  | Unconfirmed. Confirm scope before writing the suite                                      |
| Request game mode because one screen is hard to target | Exact wording and neighbor context resolve most of these — see [Tap](/writing-tests/tap) |

## Next

* [MAP\_ACTION](/writing-tests/tap/map-action)
* [Devices](/running-tests/devices)
* [Known limitations](/reference/known-limitations)

***

*Last updated: 6 August 2026*


# Reading a Report

Every run produces a report — per-step screenshots, timestamped logs, a video recording and DT usage.

Every run produces a report: what each step did, what the screen looked like before and after, what Drizz decided, and what it cost.

|                      |                                                                          |
| -------------------- | ------------------------------------------------------------------------ |
| **Platforms**        | Android · iOS                                                            |
| **Every report has** | Per-step status · before/after screenshots · timestamped logs · DT usage |
| **Three types**      | Standard · Debug · Accessibility                                         |
| **Links**            | Signed and time-limited — they expire                                    |

## Prerequisites

* A completed run, local or cloud
* Access to the project the run belongs to

## What's in every report

| Element                          | Contents                                                                                                   |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| **Per-step status**              | Passed, failed, healed or skipped, for every step in order                                                 |
| **Before and after screenshots** | The screen going into the step, and coming out of it                                                       |
| **Timestamped logs**             | What Drizz decided at each step and why, with the duration                                                 |
| **A video recording**            | The whole run, downloadable from the Actions section — see [Recordings](/reports-and-debugging/recordings) |
| **Token usage**                  | DT spent on the run                                                                                        |
| **The device that ran it**       | Model and OS version                                                                                       |

Steps that resolved from cache are marked *"Predicted Action drizzing fast ⚡"* — see [Caching](/running-tests/caching). Steps repaired mid-run carry a healed badge — see [Self-healing](/running-tests/self-healing).

## Read a report

1. Read the run status: `Passed`, `Passed (healed)`, `Failed`, `Error` or `Blocked`. `Error` and `Blocked` are not test failures — see [Statuses](/reports-and-debugging/statuses).
2. Read the healed-step count, including on a green run. Healed steps have weak descriptions.
3. Open the **first** failing step, not the last. A cascade of failed steps has one cause at the top.
4. Open that step's **before**-screenshot and compare it against the screen the step expected — see [When a step fails](/reports-and-debugging/when-a-step-fails).
5. Open the video only if the stills don't explain the failure. Use it for timing, animation and navigation problems.

## Report types

{% tabs %}
{% tab title="Standard" %}
The run, step by step, with before and after screenshots, timestamped logs, the recording and DT usage.

This is the default report and is produced for every run.
{% endtab %}

{% tab title="Debug" %}
Adds deeper diagnostic detail beyond the standard report.

Applies when the standard report does not explain a failure.
{% endtab %}

{% tab title="Accessibility" %}
Adds accessibility findings from the run, alongside the functional result.

Drizz validates what is visually rendered. It does **not** do WCAG conformance scanning, screen reader testing, contrast analysis, or semantic and role-level validation — see [Known limitations](/reference/known-limitations).
{% endtab %}
{% endtabs %}

## Sharing a report

Report links are **signed and time-limited**. They open for anyone holding the link until the link expires, then stop.

| Use                                                        | Supported |
| ---------------------------------------------------------- | --------- |
| Share for immediate review — a message, a live triage call | ✅         |
| Attach to a ticket or a release record                     | Link      |

## Getting results out

There is no export format. Reports cannot be exported as JUnit XML, JSON or Allure, so CI systems cannot ingest results directly. With no run-status endpoint, a pipeline can trigger a Drizz run but cannot wait on it or gate a build on the outcome. Artifacts download by hand from the Actions section.

## Common mistakes

| What you do                                       | What happens                                                           |
| ------------------------------------------------- | ---------------------------------------------------------------------- |
| Debug the last red step                           | It's a consequence. The first failing step is the cause                |
| Read `Passed (healed)` as `Passed`                | The suite is held together by repairs and the pass rate hides it       |
| Paste a report link into a ticket as the evidence | The link expires and the ticket becomes unusable. Attach the artifacts |
| Open the debug report first                       | The standard report's before-screenshot answers most failures faster   |
| Treat `Error` as a test failure                   | It's infrastructure, not your test. Re-run it before investigating     |
| Expect CI to parse the report                     | No export format exists. There is nothing to parse                     |

## Next

* [Statuses](/reports-and-debugging/statuses)
* [When a step fails](/reports-and-debugging/when-a-step-fails)
* [Recordings](/reports-and-debugging/recordings)

***

*Last updated: 6 August 2026*


# Statuses

What Passed, Passed (healed), Failed, Error and Blocked each mean, and why infrastructure failures now report as Error.

A run reports one of five statuses. Two of them — `Error` and `Blocked` — are not test results.

|                         |                                                                    |
| ----------------------- | ------------------------------------------------------------------ |
| **Statuses**            | Passed · Passed (healed) · Failed · Error · Blocked                |
| **Your test's fault**   | Failed, and only Failed                                            |
| **Not your test**       | Error (infrastructure) · Blocked (billing)                         |
| **Green but not clean** | Passed (healed) — check which steps needed repair                  |
| **Watch out**           | Infrastructure failures now report as **Error**, not **Cancelled** |

## Prerequisites

None.

## Status reference

| Status              | Meaning                                                                              | Cause                                                                 | Action                                                                                                |
| ------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **Passed**          | Every step succeeded first time                                                      | —                                                                     | None                                                                                                  |
| **Passed (healed)** | Succeeded, but one or more steps were repaired mid-run                               | A step description no longer matches the screen                       | Read the healed list and rewrite those descriptions — see [Self-healing](/running-tests/self-healing) |
| **Failed**          | A step failed and could not be recovered                                             | The test, the app, or the screen state                                | Triage it — see [When a step fails](/reports-and-debugging/when-a-step-fails)                         |
| **Error**           | Something outside the test went wrong. Most commonly a device could not be allocated | Infrastructure. Nothing in the script caused it                       | Re-run. If it recurs, it's device capacity — see [Devices](/running-tests/devices)                    |
| **Blocked**         | The run was stopped for a billing reason and never started                           | Wallet out of credit. It does not always present as a billing message | Check the wallet balance and top up — see [Billing & tokens](/your-account/billing-and-tokens)        |

## Common mistakes

| What you do                                               | What happens                                                       |
| --------------------------------------------------------- | ------------------------------------------------------------------ |
| Still parsing `Cancelled`                                 | Every infrastructure failure is missing from your numbers          |
| Debug an `Error` as a test failure                        | You investigate a script that never ran                            |
| Count `Passed (healed)` as `Passed` in a pass-rate metric | The metric hides a degrading suite                                 |
| Treat `Blocked` as a platform outage                      | It's the wallet. Check the balance first                           |
| Re-run a `Failed` test hoping it passes                   | A description failure is deterministic. Read the before-screenshot |

## Next

* [When a step fails](/reports-and-debugging/when-a-step-fails)
* [Reading a report](/reports-and-debugging/reading-a-report)
* [Self-healing](/running-tests/self-healing)

***

*Last updated: 6 August 2026*


# When a step fails

Triage procedure for a failed step, and the before-screenshot table that maps symptom to cause to fix.

The step that failed is often not the step that is broken. Diagnosis starts at the before-screenshot of the first failing step.

|                        |                                                                              |
| ---------------------- | ---------------------------------------------------------------------------- |
| **Platforms**          | Android · iOS                                                                |
| **Start with**         | The **before**-screenshot of the first failing step                          |
| **Most common cause**  | The description doesn't match the app's exact on-screen wording              |
| **Second most common** | An earlier step didn't do what you thought, so you're on the wrong screen    |
| **Watch out**          | Debug the *first* failing step, not the last. A cascade of red has one cause |

## Prerequisites

Gather these before triaging:

* The report for the failed run
* The **first** failing step in the step list, not the last
* That step's before-screenshot
* The healed-step count for the run
* The device model and OS version the run used

## Why the reported step is not always the broken step

A tap fails because the element is absent. The element is absent because the run is on the wrong screen. The run is on the wrong screen because an earlier step tapped something adjacent, ran before the screen finished loading, or was consumed by a popup. Drizz reports the failure at the point it surfaced, which is the last step in that chain.

The first question is whether the run is on the expected screen. The before-screenshot answers it.

## Triage procedure

1. Open the debug report and locate the **first** failing step.
2. Open that step's **before**-screenshot.
3. Compare it against the screen the step expected.
4. If the screen is wrong, walk back up the step list to the last step whose after-screenshot looked correct. The step immediately after it is the defect. Stop here.
5. If the screen is correct, match the before-screenshot against the symptom table below and apply the fix.
6. Check whether the step carries a healed badge. A healed step has a weak description — rewrite it now — see [Self-healing](/running-tests/self-healing).
7. If the step resolved from cache and the UI changed recently, re-run. If it is still wrong on the third run, the description is the cause — see [Caching](/running-tests/caching).
8. Re-run the test and confirm the step passes.
9. If it still fails, use **Report Issue** in the desktop app. It bundles logs, screenshots and device state automatically.

## Symptom, cause, fix

<table data-search="false"><thead><tr><th>Symptom in the before-screenshot</th><th>Cause</th><th>Fix</th></tr></thead><tbody><tr><td><strong>Wrong screen entirely</strong></td><td>An earlier step didn't do what you thought</td><td>Fix the earlier step. This step is correct</td></tr><tr><td><strong>Right screen, element off-screen</strong></td><td>Missing scroll</td><td>Add <code>Scroll down until "&#x3C;target>" is visible</code> before the step</td></tr><tr><td><strong>Right screen, element present</strong></td><td>Description doesn't match, or is ambiguous</td><td>Use the app's exact on-screen text; add neighbor or section context</td></tr><tr><td><strong>Screen mid-load, spinner visible</strong></td><td>Missing wait</td><td>Add <code>Wait Until &#x3C;n> Seconds</code> before the step</td></tr><tr><td><strong>Keyboard covering the element</strong></td><td>Keyboard handling</td><td>See <a href="/pages/fmEafMB0dQcmof1UPSdo">Type</a></td></tr><tr><td><strong>Popup covering everything</strong></td><td>Unhandled blocker</td><td>Add a blocker rule in Memory — see <a href="/pages/wl2GqBAWrbDmrlNZGHGT">Memory &#x26; blockers</a></td></tr><tr><td><strong>Black or blank screen</strong></td><td>Secure screen — the OS blocks capture on payment and PIN screens</td><td>Cannot be automated. Restructure the test around it</td></tr><tr><td><strong>Step carries a healed badge</strong></td><td>The description no longer matches the screen</td><td>Rewrite the description — see <a href="/pages/WhpvSc0GZjCheriEDXSF">Self-healing</a></td></tr><tr><td><strong>Cached step acting on a moved element</strong></td><td>Stale resolution after a UI change</td><td>Re-run. If wrong on the third run, fix the description — see <a href="/pages/4NRiHtnuovujdAZq5Ptr">Caching</a></td></tr></tbody></table>

## Fixing a description that doesn't match

Three edits, in order of how often they resolve the failure.

```
# 1 — use the app's exact visible wording, not a paraphrase
Tap on Add to Cart

# 2 — add the element type when the label alone appears twice
Tap on the Add to Cart button

# 3 — anchor to a neighbor or a section when the label repeats in a list
Tap on the plus icon for Running Shoes
Tap on % option under Discount
```

Paraphrasing is the most common cause of a tap landing on the wrong element. If the button reads `Charge Now`, write `Charge Now`.

## Common mistakes

| What you do                                      | What happens                                                        |
| ------------------------------------------------ | ------------------------------------------------------------------- |
| Debug the last red step in a cascade             | You fix a symptom and the run fails one step earlier next time      |
| Re-run and hope                                  | A description failure is deterministic. It fails again              |
| Add a long wait to fix a wrong-element failure   | The suite gets slower and the tap still lands on the wrong element  |
| Handle a global popup with an `IF` in every test | Put it in Memory once — one rule, every test                        |
| Read the after-screenshot to diagnose            | It shows the aftermath. The *before* shows the cause                |
| Report a black screen as a Drizz bug             | Secure screens block capture for every tool. It cannot be automated |

## Next

* [Statuses](/reports-and-debugging/statuses)
* [Troubleshooting](/reports-and-debugging/troubleshooting)
* [Self-healing](/running-tests/self-healing)

***

*Last updated: 6 August 2026*


# Recordings

Every run is recorded end to end. Download the video, screenshots and logs from the Actions section of a test attempt.

Every run is recorded end to end — a full-motion replay of UI actions, transitions and responses, downloadable per test attempt.

|                   |                                                                           |
| ----------------- | ------------------------------------------------------------------------- |
| **Platforms**     | Android · iOS                                                             |
| **Recorded**      | Every run, automatically. Nothing to enable                               |
| **Download from** | The **Actions** section of a test attempt                                 |
| **Also there**    | Screenshots and execution logs                                            |
| **Watch out**     | Secure screens render black in the recording. The OS blocks capture there |

## Prerequisites

* A completed run, local or cloud
* The specific test attempt you want artifacts for — each attempt has its own set

## Download the artifacts

1. Open the run.
2. Open the test attempt.
3. Open the **Actions** section.
4. Download the recording, the screenshots or the execution logs.
5. Confirm the downloaded file opens. Report links are signed and time-limited; the downloaded file is not.

| Artifact           | Contents                                                       |
| ------------------ | -------------------------------------------------------------- |
| **Recording**      | The full run, replayable frame by frame                        |
| **Screenshots**    | The before and after images for every step                     |
| **Execution logs** | Timestamped, step by step, with what Drizz decided at each one |

Each test attempt has its own set. A test that ran three times in a plan has three recordings.

## What a recording shows that a screenshot cannot

<table data-search="false"><thead><tr><th>Case</th><th>Detail</th></tr></thead><tbody><tr><td><strong>Transient UI</strong></td><td>A toast, error banner or loader that appeared and went between screenshots</td></tr><tr><td><strong>Animation and timing</strong></td><td>A screen still settling when the step ran</td></tr><tr><td><strong>Unexpected navigation</strong></td><td>A tap that opened something and closed it again</td></tr><tr><td><strong>Failures that don't reproduce locally</strong></td><td>Diagnose from the recording rather than rebuilding the scenario</td></tr><tr><td><strong>Shared evidence</strong></td><td>QA, engineering and product review the same footage</td></tr><tr><td><strong>Audit trail</strong></td><td>A verifiable artifact of exactly what ran</td></tr><tr><td><strong>Secure screens</strong></td><td>Rendered black. The OS blocks capture there</td></tr></tbody></table>

{% hint style="success" %}
Read the report first, recording second. The before-screenshot of the first failing step resolves most failures on its own — see [When a step fails](/reports-and-debugging/when-a-step-fails).
{% endhint %}

## Retention

Report links are **signed and time-limited**, and so is access to the artifacts behind them. A recording that is evidence for a bug report or a release decision has to be downloaded and attached as a file.

## Common mistakes

| What you do                                          | What happens                                                       |
| ---------------------------------------------------- | ------------------------------------------------------------------ |
| Link a recording in a ticket instead of attaching it | The link expires and the ticket loses its evidence                 |
| Open the recording before reading the report         | You watch four minutes of video to find what one screenshot showed |
| Report a black section as a broken recording         | Secure screens block capture. Nothing was recorded to lose         |
| Download from the plan rather than the attempt       | Artifacts are per test attempt. A re-run has its own set           |

## Next

* [Reading a report](/reports-and-debugging/reading-a-report)
* [When a step fails](/reports-and-debugging/when-a-step-fails)
* [Troubleshooting](/reports-and-debugging/troubleshooting)

***

*Last updated: 6 August 2026*


# Troubleshooting

Symptom to fix, in three tables: devices that won't connect, sign-in and network problems, and runs that misbehave.

Find the symptom, apply the fix. Three groups: devices, sign-in and network, runs.

|                                  |                                                                                                    |
| -------------------------------- | -------------------------------------------------------------------------------------------------- |
| **Platforms**                    | Android · iOS                                                                                      |
| **Run won't start, vague error** | Check your wallet balance first                                                                    |
| **Run waits without starting**   | No free device, or a lockable pool is fully leased                                                 |
| **Every step slow**              | Cold cache, or caching isn't on for your app                                                       |
| **Watch out**                    | Still stuck after the tables? Use **Report Issue** — it bundles logs, screenshots and device state |

## Prerequisites

Gather these before working through the tables:

* The exact symptom and the screen it appears on
* The run status: `Passed`, `Passed (healed)`, `Failed`, `Error` or `Blocked`
* The device model, OS version and whether it is local, cloud or private
* The desktop app version, and whether the same symptom occurs on another network
* Your wallet balance
* `adb devices` output, for an Android connection problem

## Device won't connect

<table data-search="false"><thead><tr><th>Symptom</th><th>Try</th></tr></thead><tbody><tr><td>Android device not listed</td><td>Run <code>adb devices</code>. If it says <code>unauthorized</code>, accept the prompt on the phone. If it's absent, check the cable</td></tr><tr><td>Emulator listed but won't connect</td><td>adb is stuck or the emulator is still booting — Drizz states which. Wait, or run <code>adb kill-server &#x26;&#x26; adb start-server</code></td></tr><tr><td>Device locked</td><td>Unlock it. Drizz refuses a locked device</td></tr><tr><td>Duplicate emulator launched</td><td>Drizz attaches to a running emulator rather than booting a second. If two appear, close both and reconnect</td></tr><tr><td>iOS simulator won't connect</td><td>Confirm Xcode is installed <em>and has been opened once</em></td></tr><tr><td>iOS "Preparing device" hangs</td><td>Disconnect, re-trust the Mac on the device, reconnect</td></tr><tr><td>WebDriverAgent signature failure</td><td>The Apple team can't sign. Check the team picker — free teams expire weekly</td></tr><tr><td>App won't install on a real iPhone</td><td>The build is a simulator build, or an App Store version of the same bundle ID is installed. Uninstall it first</td></tr></tbody></table>

## Sign-in and network

| Symptom                                      | Try                                                                                          |
| -------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Sign-in completes but the app doesn't log in | Known issue. Quit and relaunch. If it persists, use Report Issue                             |
| Personal email rejected                      | Working as designed. Sign in with your work email                                            |
| Signed in but no apps or files               | Org assignment, or a trial not yet approved. Contact Drizz support. Do not re-install        |
| Files you had before have vanished           | An org-assignment problem, not data loss. Contact Drizz support with your email and org name |
| Works on a hotspot, not on office wifi       | Corporate TLS inspection. Run **Help → Network Preflight**                                   |
| Sign-in hangs on a corporate network         | Corporate TLS inspection. Run Network Preflight first                                        |

## Runs

<table data-search="false"><thead><tr><th>Symptom</th><th>Try</th></tr></thead><tbody><tr><td>Run stuck on "Stopping"</td><td>Drizz recovers this after a timeout. If it doesn't, restart the desktop app</td></tr><tr><td>Run won't start, vague server error</td><td>Check the wallet balance. An exhausted balance does not always present as a billing message</td></tr><tr><td>Run waits without starting</td><td>No free device, or a lockable pool is fully leased — see <a href="/pages/VVqSsKwCSmcNlUyKnWkE">Devices</a></td></tr><tr><td>Runs wait while devices sit idle</td><td>The block is data, not hardware. The lockable pool is smaller than the concurrency setting</td></tr><tr><td>Every step slow</td><td>Cache is cold, or caching isn't enabled for your app — see <a href="/pages/4NRiHtnuovujdAZq5Ptr">Caching</a></td></tr><tr><td>Suite got slower after a release</td><td>Expected. The UI changed and the cache is rebuilding</td></tr><tr><td>Tap fails right after typing</td><td>The keyboard is covering the element — see <a href="/pages/fmEafMB0dQcmof1UPSdo">Type</a></td></tr><tr><td>Screen is black in the report</td><td>Secure screen. The OS blocks capture there, for every tool</td></tr><tr><td>Test plan references a missing app</td><td>The build may have expired — see <a href="/pages/vThWqLQMHIlnE1l3Avr6">Test plans</a></td></tr><tr><td>Run status reads <code>Error</code></td><td>Infrastructure, not your test. Re-run. If it recurs, it's device capacity</td></tr><tr><td>Run status reads <code>Blocked</code></td><td>Billing. Top up the wallet</td></tr><tr><td>Nothing counted as <code>Cancelled</code> any more</td><td>Infrastructure failures now report as <code>Error</code> — see <a href="/pages/75PJpg9l527HNAFO85yu">Statuses</a></td></tr><tr><td>Green run, but a step healed</td><td>The description is weak. Rewrite it — see <a href="/pages/WhpvSc0GZjCheriEDXSF">Self-healing</a></td></tr><tr><td>iOS run much slower than the Android one</td><td>Expected. iOS steps run roughly twice as slow</td></tr><tr><td><code>CLEAR_APP</code> leaves state behind on iOS</td><td>A no-op there. Reset in-app, or use a fresh install</td></tr></tbody></table>

## Still stuck

1. Open the desktop app.
2. Select **Report Issue**.
3. Submit. It bundles your logs, recent screenshots and device state and sends them to Drizz support.

## Next

* [When a step fails](/reports-and-debugging/when-a-step-fails)
* [Statuses](/reports-and-debugging/statuses)
* [Reading a report](/reports-and-debugging/reading-a-report)

***

*Last updated: 6 August 2026*


# Organizations & Access

How a Drizz organization is created, what is shared inside one, and what to do when files are missing after sign-in.

Every test, module, dataset, app and report belongs to an organization, not to an individual user.

|               |                                                                                      |
| ------------- | ------------------------------------------------------------------------------------ |
| **Created**   | Automatically, when the first person from an email domain signs in                   |
| **Joining**   | Every later sign-in from the same domain joins that organization                     |
| **Sign-in**   | Work email only — personal domains are rejected                                      |
| **Scope**     | Tests, modules, datasets, Memory, registered apps, reports and the wallet are shared |
| **Watch out** | Missing files after sign-in is an organization assignment, not data loss             |

## Prerequisites

* A work email address on your company's domain
* Drizz desktop app installed, or web app access
* Trial or account approved for your organization

## How an organization is created

1. The first person from `yourcompany.com` signs in. Drizz creates the organization.
2. Everyone who signs in from that domain afterward joins the same organization.

There is no self-service organization creation and no invite link.

## What is shared

| Shared org-wide        | Effect                                                                  |
| ---------------------- | ----------------------------------------------------------------------- |
| Tests and modules      | A module written by one member is callable by every member              |
| Datasets               | Includes lockable pools, which lease rows across the whole organization |
| Memory                 | Blocker rules and app context apply to every member's runs              |
| Registered apps        | One upload serves every test plan                                       |
| Reports and recordings | Any member can open any run                                             |
| Wallet                 | One balance, drawn down by every member's runs                          |

A large regression run started by one member reduces the balance available to everyone. See [Billing & tokens](/your-account/billing-and-tokens).

## Sign-in domains

| Rule                           | Detail                                                  |
| ------------------------------ | ------------------------------------------------------- |
| Work email required            | Personal domains — Gmail, Outlook, Yahoo — are rejected |
| Domain determines organization | The email domain is the only routing signal             |
| Second company domain          | Creates a second, empty organization                    |
| Contractor on their own domain | Lands in a separate organization from the client team   |

To be moved between organizations, email `support@drizz.dev` with your email address and the target organization name.

## Files missing after sign-in

An empty file list has two causes. Drizz does not delete work on sign-in, and a reinstall does not change organization assignment.

| Cause                                        | Symptom                                  | Fix                                                                     |
| -------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------- |
| Account assigned to a different organization | Files from a previous session are absent | Email `support@drizz.dev` with your email address and organization name |
| Trial not yet approved                       | Sign-in succeeds, no content appears     | Email `support@drizz.dev` to confirm approval status                    |

To resolve:

1. Confirm the email address you signed in with, including any alias.
2. Email `support@drizz.dev` with that address and your organization name.
3. Ask which organization the account is assigned to.

## Common mistakes

| What you did                                           | What happens                                                        |
| ------------------------------------------------------ | ------------------------------------------------------------------- |
| Signed up with a personal email                        | Sign-in is rejected. Use your work email                            |
| Reinstalled the desktop app because files were missing | Nothing changes — a reinstall cannot fix an organization assignment |
| Assumed a teammate cannot see your test                | They can. Everything is org-scoped                                  |
| Ran a large plan without checking the wallet           | The run blocks everyone else's runs, not just yours                 |
| Signed in with a second work domain                    | A second, empty organization is created                             |

## Next

* [Billing & tokens](/your-account/billing-and-tokens) — metering and the shared wallet
* [Managing apps](/your-account/managing-apps) — registering builds for cloud runs
* [Troubleshooting](/reports-and-debugging/troubleshooting) — sign-in and network symptoms

***

*Last updated: 6 August 2026*


# Billing & Tokens

How Drizz meters test runs in DT, what drives the cost of a step, what each wallet state means, and how to top up.

Drizz meters usage in **DT (Drizz Tokens)**, deducted per test step from a single wallet shared by the organization.

|                      |                                                                                     |
| -------------------- | ----------------------------------------------------------------------------------- |
| **Unit**             | DT (Drizz Tokens), deducted per step                                                |
| **What drives cost** | How the step was resolved — Cached lowest, AI highest                               |
| **Billing event**    | One per step, regardless of retries                                                 |
| **Wallet**           | One shared balance per organization                                                 |
| **Watch out**        | An exhausted wallet surfaces as a server error — check the balance before debugging |

## Prerequisites

* Drizz organization account
* Access to the organization billing page
* A payment card, for top-ups

## How a step is metered

Every step falls into one of four categories. The first signal wins. Each step is billed once, even if it retried.

| Category   | When it applies                                    | Relative cost |
| ---------- | -------------------------------------------------- | ------------- |
| **Cached** | Drizz has resolved this step on this screen before | Lowest        |
| **System** | A system command, or a navigation step             | Low           |
| **OCR**    | Resolved by reading text on screen                 | Moderate      |
| **AI**     | Vision AI ran to locate the element                | Highest       |

Cache coverage builds across runs, so the DT cost of a test plan falls as it is re-run. See [Caching](/running-tests/caching).

Per-category DT rates and the DT-to-currency rate are shown on your organization's billing page, which is authoritative. They are not repeated here because they change.

## Where to see usage

| Where                            | Shows                                    |
| -------------------------------- | ---------------------------------------- |
| Wallet badge, desktop app header | Current organization balance             |
| Run banner                       | DT used by the run in progress           |
| Report                           | DT used, for a finished run              |
| Organization billing page        | Balance, history, current rates, top-ups |

The wallet badge can show a stale balance after a top-up. Navigate away and back to refresh it.

## Wallet states

| State       | Means                | Effect on runs                                |
| ----------- | -------------------- | --------------------------------------------- |
| **Healthy** | Normal balance       | Runs start as expected                        |
| **Low**     | Balance running down | Runs start. Top up before it blocks a release |
| **Blocked** | No credit remaining  | Runs will not start                           |

A blocked wallet does not always report itself as a billing error. It surfaces as a server error, an availability error, or a run that does not start.

## Top up

1. Open your organization's billing page.
2. Select top-up.
3. Pay by card.
4. Confirm the wallet badge shows the new balance. Navigate away and back if it is stale.

The balance is shared, so a top-up by any member unblocks every member.

Enterprise accounts can be configured to allow runs to continue past a zero balance. Ask your Drizz contact.

## Common mistakes

| What you did                                          | What happens                                                     |
| ----------------------------------------------------- | ---------------------------------------------------------------- |
| Compared cost across two runs of a new plan           | Cache coverage differs between runs — early runs cost more       |
| Debugged a "server error" before checking the balance | An empty wallet surfaces as an unrelated-looking failure         |
| Assumed the wallet is per-user                        | It is shared org-wide. Another member's regression run drains it |
| Quoted a DT rate from a document or a deck            | Rates change. Read them from the billing page                    |

## Next

* [Organizations & access](/your-account/organizations-and-access) — why the wallet is shared
* [Caching](/running-tests/caching) — how steps become cheap
* [Reading a report](/reports-and-debugging/reading-a-report) — per-run usage

***

*Last updated: 6 August 2026*


# Managing Apps

Register app builds with Drizz so test plans and Memory can use them — the 500 MB cap, iOS build types, and what cannot be downloaded back.

A test plan installs a **registered app** — a build uploaded to Drizz and held against its package name or bundle ID plus a version.

|                  |                                                                    |
| ---------------- | ------------------------------------------------------------------ |
| **Platforms**    | Android (`.apk`) · iOS (`.ipa`)                                    |
| **Upload limit** | 500 MB per binary                                                  |
| **iOS**          | Test plans require a real-device build, not a simulator build      |
| **Also used by** | Memory — blocker rules and app context attach to a registered app  |
| **Watch out**    | A build cannot be downloaded back out of Drizz. Keep your own copy |

## Prerequisites

* Drizz organization account
* A signed release build under 500 MB — `.apk` (Android) or `.ipa` for a real device (iOS)
* Web app access

## What a registered app provides

| Capability        | Detail                                                            |
| ----------------- | ----------------------------------------------------------------- |
| Test plan install | A plan selects a registered version and installs it on the device |
| Memory attachment | App context and blocker rules attach to the registered app        |
| API trigger       | CI triggers a run against a named version                         |
| Scope             | Org-scoped. One upload serves every test plan in the organization |

## Register a build

1. Open the registered apps list in the web app.
2. Select upload.
3. Choose the `.apk` or `.ipa` file. Drizz reads the package name, bundle ID and version from the binary.
4. Confirm the new version appears in the list before binding it to a test plan.

To upload from a pipeline instead, see [Upload a build](/automate-and-integrate/upload-a-build) — same 500 MB cap, same formats.

## Limits

| Limit                     | Value                                                  |
| ------------------------- | ------------------------------------------------------ |
| Maximum binary size       | 500 MB                                                 |
| Accepted formats          | `.apk` (Android), `.ipa` (iOS)                         |
| Oversized upload          | Rejected, not truncated                                |
| Duplicate version         | Conflicts with the existing version — bump the version |
| Download a build back out | Not supported                                          |

To bring a binary under 500 MB, strip debug symbols, drop unused resources, or split by ABI and upload the split under test.

## Build types

{% tabs %}
{% tab title="Android" %}

* Upload a `.apk`.
* The package name and version code are read from the binary.
  {% endtab %}

{% tab title="iOS" %}

* Upload an `.ipa` built and signed for a **real device**.
* A simulator build uploads successfully and then fails at install time, when the test plan runs.
  {% endtab %}
  {% endtabs %}

## Builds cannot be retrieved

Drizz does not serve the binary back. Keep every uploaded build in your own artifact store or CI cache.

## Common mistakes

| What you did                               | What happens                                                   |
| ------------------------------------------ | -------------------------------------------------------------- |
| Uploaded a simulator build for an iOS plan | Upload blocked by Drizz                                        |
| Deleted your local copy after uploading    | The binary is gone. Drizz does not return it                   |
| Uploaded a 700 MB debug build              | Rejected at the 500 MB cap                                     |
| Re-uploaded the same version number        | Conflicts with the existing version — bump the version         |
| Assumed a build is kept indefinitely       | Retention is not confirmed. A plan can fail on a missing build |

## Next

* [Test plans](/running-tests/test-plans) — selecting an app for a plan
* [Upload a build](/automate-and-integrate/upload-a-build) — registering from CI
* [Organizations & access](/your-account/organizations-and-access) — why apps are shared

***

*Last updated: 6 August 2026*


# API Overview

The Drizz REST API in one page — the auth model, the 24-hour token, rate limits, and the three calls that get a build running.

The Drizz REST API covers three operations: authenticate, upload a build, trigger a test plan.

In the examples below, the shell variable `DRIZZ_BASE_URL` holds that value, and `DRIZZ_API_KEY` holds the access token returned by the auth call.

{% hint style="info" %}
**At a glance**

`POST <DRIZZ_API_BASE_URL>/testplan/run`
{% endhint %}

|                |                                                                                    |
| -------------- | ---------------------------------------------------------------------------------- |
| **Auth**       | OAuth 2.0 client credentials → a bearer token, sent as `x-api-key`                 |
| **Token life** | 24 hours (`expires_in: 86400`)                                                     |
| **Rate limit** | \~4 requests/second, burst \~20, per client → HTTP 429 over that                   |
| **Upload cap** | 500 MB per binary                                                                  |
| **Watch out**  | There is no endpoint to poll a run or fetch results. Trigger is where the API ends |

## Prerequisites

* Active Drizz organization account
* Client ID and client secret, issued by Drizz
* Auth host and audience string, issued by Drizz
* API base URL for your organization
* At least one registered app, with its package name or bundle ID
* At least one test plan, with its ID
* `curl` and `jq`, or an equivalent HTTP client

## Copy this

```bash
# 1 — set these once
export DRIZZ_BASE_URL="<DRIZZ_API_BASE_URL>"
export DRIZZ_AUTH_DOMAIN="<auth-domain>"
export DRIZZ_CLIENT_ID="<your_client_id>"
export DRIZZ_CLIENT_SECRET="<your_client_secret>"
export DRIZZ_AUDIENCE="<DRIZZ_API_AUDIENCE>"

# 2 — authenticate. The token is valid for 24 hours
export DRIZZ_API_KEY=$(curl -s -X POST "https://${DRIZZ_AUTH_DOMAIN}/oauth/token" \
  -H "Content-Type: application/json" \
  -d "{
    \"client_id\": \"${DRIZZ_CLIENT_ID}\",
    \"client_secret\": \"${DRIZZ_CLIENT_SECRET}\",
    \"audience\": \"${DRIZZ_AUDIENCE}\",
    \"grant_type\": \"client_credentials\"
  }" | jq -r .access_token)

# 3 — upload the build. Skip this if the version is already registered
curl -s -X POST "${DRIZZ_BASE_URL}/apk/upload" \
  -H "x-api-key: ${DRIZZ_API_KEY}" \
  -F "file=@app-release.apk"

# 4 — trigger the test plan. Returns an execution_id
curl -s -X POST "${DRIZZ_BASE_URL}/testplan/run" \
  -H "x-api-key: ${DRIZZ_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "test_plan_id": "ADuF4ViN",
    "apks": { "com.shopease.android": "1.0.0" }
  }'
```

## Endpoints

| #  | Call            | Method and endpoint                            | Required                           | Returns                                      |
| -- | --------------- | ---------------------------------------------- | ---------------------------------- | -------------------------------------------- |
| 1  | Authenticate    | `POST https://<auth-domain>/oauth/token`       | Yes                                | `access_token`, valid 24 hours               |
| 2  | Upload a build  | `POST <DRIZZ_API_BASE_URL>/apk/upload`         | No — only when the version changes | Package name, version name, version code     |
| 3  | Trigger a run   | `POST <DRIZZ_API_BASE_URL>/testplan/run`       | Yes                                | `execution_id`                               |
| 3b | Trigger a batch | `POST <DRIZZ_API_BASE_URL>/testplan/run/batch` | No                                 | `successful_executions`, `failed_executions` |

If the version under test is already registered, skip call 2 and name the version in `apks` on call 3.

## Request headers

| Header         | Value                           | Applies to                                             |
| -------------- | ------------------------------- | ------------------------------------------------------ |
| `x-api-key`    | Access token from the auth call | Every call except `/oauth/token`                       |
| `Content-Type` | `application/json`              | `/oauth/token`, `/testplan/run`, `/testplan/run/batch` |
| `Content-Type` | `multipart/form-data`           | `/apk/upload`                                          |

The token header is `x-api-key`, not `Authorization: Bearer`. An `Authorization: Bearer` header is ignored and the call is rejected as unauthenticated. See [Authenticate](/automate-and-integrate/authenticate).

## Scope

| In scope                                                  | Not in scope                                               |
| --------------------------------------------------------- | ---------------------------------------------------------- |
| Authenticating an external system with client credentials | Polling a run's status                                     |
| Uploading a mobile app binary                             | Fetching results, reports or artifacts                     |
| Triggering one test plan                                  | JUnit, JSON or Allure export                               |
| Triggering several test plans in a batch                  | Webhooks or any callback on completion                     |
|                                                           | Creating or editing test plans, tests, modules or datasets |

A pipeline can start a run. It cannot wait for one or gate a build on the outcome. See [CI/CD](/automate-and-integrate/ci-cd).

## Rate limits

| Limit           | Value                                                                |
| --------------- | -------------------------------------------------------------------- |
| Sustained rate  | \~4 requests per second, per client                                  |
| Burst allowance | \~20 requests                                                        |
| Over the limit  | HTTP 429                                                             |
| Batch of plans  | Use `POST /testplan/run/batch` — one request instead of one per plan |

Back off with jitter after a 429. See [Errors & limits](/automate-and-integrate/errors-and-limits).

## Error codes

| Code    | Meaning           | What to do                                                            |
| ------- | ----------------- | --------------------------------------------------------------------- |
| **200** | Success           | Request accepted                                                      |
| **207** | Multi-Status      | Batch partially succeeded — inspect `failed_executions`               |
| **400** | Bad Request       | Validate the payload and parameters                                   |
| **404** | Not Found         | Verify the test plan ID and that the app version is registered        |
| **409** | Conflict          | This app version already exists — bump the version or skip the upload |
| **429** | Too Many Requests | Over the rate limit. Back off and retry                               |
| **500** | Server Error      | Contact `support@drizz.dev`                                           |
| **502** | Bad Gateway       | Retry, or check service availability                                  |

## Common mistakes

| What you did                                | What happens                                      |
| ------------------------------------------- | ------------------------------------------------- |
| Sent `Authorization: Bearer <token>`        | Rejected. The header is `x-api-key`               |
| Cached the token for a week                 | It expired after 24 hours. Requests start failing |
| Looped single triggers for 40 plans         | HTTP 429. Use the batch endpoint                  |
| Waited for the run to finish in your script | There is nothing to poll. The API ends at trigger |
| Uploaded the build on every pipeline run    | Re-uploading an existing version returns 409      |

## Next

* [Authenticate](/automate-and-integrate/authenticate) — get a token
* [Trigger a run](/automate-and-integrate/trigger-a-run) — start a test plan
* [Errors & limits](/automate-and-integrate/errors-and-limits) — status codes and retry behavior

***

*Last updated: 6 August 2026*


# Authenticate

Exchange client credentials for a Drizz access token and send it on every API call. Tokens are valid for 24 hours.

Every Drizz API call requires an access token, obtained by exchanging a client ID and secret through the OAuth 2.0 client credentials flow.

With those two values set, everything on this page runs as written.

|                  |                                                            |
| ---------------- | ---------------------------------------------------------- |
| **Flow**         | OAuth 2.0 client credentials                               |
| **Content-Type** | `application/json`                                         |
| **Returns**      | `access_token`, `token_type`, `expires_in`                 |
| **Valid for**    | 86400 seconds — 24 hours                                   |
| **Watch out**    | Send the token as `x-api-key`, not `Authorization: Bearer` |

## Prerequisites

* Client ID and client secret, issued by Drizz
* Auth host (`<auth-domain>`) for your organization
* Audience string (`<DRIZZ_API_AUDIENCE>`) for your organization
* `curl` and `jq`, or an equivalent HTTP client
* A CI secret store for the client secret

## Copy this

{% tabs %}
{% tab title="Inspect the response" %}

```bash
# Exchange credentials for a 24-hour access token
curl -X POST "https://<auth-domain>/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "<your_client_id>",
    "client_secret": "<your_client_secret>",
    "audience": "<DRIZZ_API_AUDIENCE>",
    "grant_type": "client_credentials"
  }'
```

{% endtab %}

{% tab title="Capture into a variable" %}

```bash
# Capture the token so the rest of the script can use it
export DRIZZ_API_KEY=$(curl -s -X POST "https://<auth-domain>/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "<your_client_id>",
    "client_secret": "<your_client_secret>",
    "audience": "<DRIZZ_API_AUDIENCE>",
    "grant_type": "client_credentials"
  }' | jq -r .access_token)
```

{% endtab %}
{% endtabs %}

## Endpoint

```
POST https://<auth-domain>/oauth/token
```

This is the only call that does not use `<DRIZZ_API_BASE_URL>`. The token is issued by the identity service, not the Drizz API host.

## Request headers

| Header         | Required | Value              |
| -------------- | -------- | ------------------ |
| `Content-Type` | Yes      | `application/json` |

## Request parameters

| Name            | Type   | Required | Max length | Description                                           |
| --------------- | ------ | -------- | ---------- | ----------------------------------------------------- |
| `client_id`     | string | Yes      | 128        | Client identifier issued to you. Example: `abc123xyz` |
| `client_secret` | string | Yes      | 256        | Client secret issued to you                           |
| `audience`      | string | Yes      | 256        | The Drizz API audience — `<DRIZZ_API_AUDIENCE>`       |
| `grant_type`    | string | Yes      | 32         | Always `client_credentials`                           |

Send all four exactly as issued. A wrong `audience` fails the same way wrong credentials do.

## Response

```json
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 86400
}
```

| Field          | Type   | Description                                  |
| -------------- | ------ | -------------------------------------------- |
| `access_token` | string | The bearer token, in JWT format              |
| `token_type`   | string | Token type — `Bearer`                        |
| `expires_in`   | number | Validity in seconds — `86400`, i.e. 24 hours |

## Use the token

Send the token in the `x-api-key` header on every authenticated Drizz API call:

```bash
curl -X POST "$DRIZZ_BASE_URL/testplan/run" \
  -H "x-api-key: $DRIZZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"test_plan_id": "ADuF4ViN", "apks": {"com.shopease.android": "1.0.0"}}'
```

`token_type` is returned as `Bearer`, but Drizz reads the token from `x-api-key`. An `Authorization: Bearer` header is ignored and the call is rejected as unauthenticated.

## Token lifetime

| Rule             | Detail                                                                                |
| ---------------- | ------------------------------------------------------------------------------------- |
| Validity         | 24 hours from issue                                                                   |
| Per pipeline run | Request one fresh token at the start of each run                                      |
| Storage          | Run-scoped only. Keep the client secret in a CI secret store, never in the repository |
| Expiry symptom   | A pipeline that worked yesterday fails today as unauthenticated                       |

## Errors

| Code    | Meaning           | What to do                                                                |
| ------- | ----------------- | ------------------------------------------------------------------------- |
| **200** | Success           | Read `access_token` from the response                                     |
| **400** | Bad Request       | Validate the payload — all four parameters, JSON body                     |
| **429** | Too Many Requests | Over the rate limit. Request one token per pipeline run, not one per call |
| **500** | Server Error      | Contact `support@drizz.dev`                                               |
| **502** | Bad Gateway       | Retry, or check service availability                                      |

The status code returned for an invalid or expired token is not confirmed. Handle it by the response body as well as the code — see [Errors & limits](/automate-and-integrate/errors-and-limits).

## Common mistakes

| What you did                            | What happens                                          |
| --------------------------------------- | ----------------------------------------------------- |
| Sent `Authorization: Bearer <token>`    | Rejected as unauthenticated. Use `x-api-key`          |
| Reused yesterday's token                | Expired after 24 hours. Request a fresh one per run   |
| Sent form-encoded credentials           | The endpoint expects `Content-Type: application/json` |
| Guessed at the `audience` value         | Fails like a bad credential. Ask your Drizz contact   |
| Committed the client secret to the repo | Anyone with repo access can trigger your runs         |

## Next

* [Upload a build](/automate-and-integrate/upload-a-build) — register a binary
* [Trigger a run](/automate-and-integrate/trigger-a-run) — start a test plan
* [API overview](/automate-and-integrate/api-overview) — the whole flow in one page

***

*Last updated: 6 August 2026*


# Upload a build

Upload an app binary to Drizz over the API so test plans can run against it. Multipart form, one file, 500 MB cap.

`POST <DRIZZ_API_BASE_URL>/apk/upload` registers an app binary so test plans can install and run it. The call is optional — skip it when the version under test is already registered.

|               |                                                                                              |
| ------------- | -------------------------------------------------------------------------------------------- |
| **Platforms** | Android (`.apk`) · iOS (`.ipa`, real-device build)                                           |
| **Body**      | `multipart/form-data`, one field: `file`                                                     |
| **Max size**  | 500 MB                                                                                       |
| **Returns**   | Package name, version name, version code                                                     |
| **Watch out** | Uploading a version that already exists returns **409** — upload only when the build changes |

## Prerequisites

* Access token from [Authenticate](/automate-and-integrate/authenticate), in `$DRIZZ_API_KEY`
* API base URL, in `$DRIZZ_BASE_URL`
* A signed build under 500 MB — `.apk` (Android) or `.ipa` for a real device (iOS)
* `curl`, or an equivalent HTTP client

## Copy this

```bash
# Upload an Android build. $DRIZZ_API_KEY is the token from the auth call
curl -X POST "$DRIZZ_BASE_URL/apk/upload" \
  -H "x-api-key: $DRIZZ_API_KEY" \
  -F "file=@app-release.apk"
```

## Endpoint

```
POST <DRIZZ_API_BASE_URL>/apk/upload
```

The same endpoint accepts iOS binaries. Drizz validates the binary, then registers it with the version metadata extracted from it, making it available to test plans, to Memory, and to a later trigger call.

## Request headers

| Header         | Required          | Value                 |
| -------------- | ----------------- | --------------------- |
| `x-api-key`    | Yes               | Your access token     |
| `Content-Type` | Set by the client | `multipart/form-data` |

With `curl -F`, `Content-Type` is set automatically. Setting it by hand drops the multipart boundary and the upload fails.

## Request parameters

| Name   | Type   | Required | Max    | Description                                |
| ------ | ------ | -------- | ------ | ------------------------------------------ |
| `file` | binary | Yes      | 500 MB | The app binary. Example: `app-release.apk` |

One field, one file per request. There is no batch upload.

## Response

```json
{
  "message": "APK uploaded successfully",
  "details": {
    "package_name": "com.shopease.android",
    "version_name": "1.0.0",
    "version_code": 100
  }
}
```

| Field                  | Type   | Description                                    |
| ---------------------- | ------ | ---------------------------------------------- |
| `message`              | string | Upload confirmation                            |
| `details.package_name` | string | Package name or bundle ID read from the binary |
| `details.version_name` | string | Human-readable version                         |
| `details.version_code` | number | Numeric version code                           |

Drizz reads the package name and version from the binary — they are not sent in the request. Use the response values to build the `apks` map for [Trigger a run](/automate-and-integrate/trigger-a-run):

```json
{ "com.shopease.android": "1.0.0" }
```

## Limits

| Limit               | Value                                       |
| ------------------- | ------------------------------------------- |
| Maximum binary size | 500 MB                                      |
| Files per request   | 1                                           |
| Duplicate version   | HTTP 409                                    |
| Rate limit          | \~4 requests/second, burst \~20, per client |

To bring a binary under 500 MB, strip debug symbols, drop unused resources, or split by ABI and upload the split under test.

## Platform notes

{% tabs %}
{% tab title="Android" %}

* Upload a `.apk`.
* The package name and version code are read from the binary.
  {% endtab %}

{% tab title="iOS" %}

* Upload an `.ipa` built and signed for a **real device**.
* A simulator build uploads successfully and then fails when the test plan installs it.
  {% endtab %}
  {% endtabs %}

## Errors

| Code    | Meaning           | What to do                                                            |
| ------- | ----------------- | --------------------------------------------------------------------- |
| **200** | Success           | Read `details` for the registered version                             |
| **400** | Bad Request       | Validate the multipart body and the `file` field                      |
| **409** | Conflict          | This app version already exists — bump the version or skip the upload |
| **429** | Too Many Requests | Over the rate limit. Back off and retry                               |
| **500** | Server Error      | Contact `support@drizz.dev`                                           |
| **502** | Bad Gateway       | Retry, or check service availability                                  |

A binary over 500 MB is rejected. See [Errors & limits](/automate-and-integrate/errors-and-limits).

## When to upload

Upload when the app version changes. Trigger against the registered version on every other run. Re-uploading a registered version returns **409 Conflict** and transfers up to 500 MB with no effect.

## Common mistakes

| What you did                                                   | What happens                                      |
| -------------------------------------------------------------- | ------------------------------------------------- |
| Set `Content-Type: multipart/form-data` by hand with `curl -F` | The boundary is lost and the upload fails         |
| Uploaded a simulator build for an iOS plan                     | Uploads fine, then fails at install on the device |
| Uploaded the same version twice                                | HTTP 409 — bump the version                       |
| Uploaded a 700 MB debug build                                  | Rejected at the 500 MB cap                        |
| Uploaded on every commit                                       | Slow pipelines, and 409s on unchanged versions    |
| Deleted your local copy afterward                              | A build cannot be downloaded back out of Drizz    |

## Next

* [Trigger a run](/automate-and-integrate/trigger-a-run) — run a plan against this build
* [Managing apps](/your-account/managing-apps) — registered apps in the UI
* [Errors & limits](/automate-and-integrate/errors-and-limits) — what 409 and the rest mean

***

*Last updated: 6 August 2026*


# Trigger a run

Start one test plan or a batch over the API. Returns an execution\_id and nothing else — there is no status endpoint.

`POST <DRIZZ_API_BASE_URL>/testplan/run` starts a test plan from outside Drizz. Drizz provisions devices, starts the plan, and returns an `execution_id`.

|                           |                                                                                         |
| ------------------------- | --------------------------------------------------------------------------------------- |
| **Batch**                 | `POST <DRIZZ_API_BASE_URL>/testplan/run/batch`                                          |
| **Headers**               | `x-api-key`, `Content-Type: application/json`                                           |
| **Returns**               | `execution_details.execution_id`                                                        |
| **Batch partial success** | HTTP **207** — check `failed_executions`                                                |
| **Watch out**             | The response confirms the run started. There is no endpoint to poll it or fetch results |

## Prerequisites

* Access token from [Authenticate](/automate-and-integrate/authenticate), in `$DRIZZ_API_KEY`
* API base URL, in `$DRIZZ_BASE_URL`
* Test plan ID, from the web app
* A registered app version for every package the plan installs
* `curl`, or an equivalent HTTP client

## Copy this

```bash
# Trigger one test plan against a registered build
curl -X POST "$DRIZZ_BASE_URL/testplan/run" \
  -H "x-api-key: $DRIZZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "test_plan_id": "ADuF4ViN",
    "apks": { "com.shopease.android": "1.0.0" }
  }'
```

## Trigger a single test plan

```
POST <DRIZZ_API_BASE_URL>/testplan/run
```

### Request headers

| Header         | Required | Value              |
| -------------- | -------- | ------------------ |
| `x-api-key`    | Yes      | Your access token  |
| `Content-Type` | Yes      | `application/json` |

### Request parameters

| Name           | Type   | Required | Max | Description                                                            |
| -------------- | ------ | -------- | --- | ---------------------------------------------------------------------- |
| `test_plan_id` | string | Yes      | 64  | The test plan to run. Example: `ADuF4ViN`                              |
| `apks`         | object | Yes      | —   | Package-to-version map. Example: `{ "com.shopease.android": "1.0.0" }` |

`apks` names the registered version each package runs against. Each version must already exist in Drizz — uploaded earlier in the pipeline, or registered previously. An unregistered version returns **404**.

### Response

```json
{
  "test_plan_id": "ADuF4ViN",
  "execution_details": {
    "status": "triggered",
    "execution_id": "exec_123456"
  }
}
```

| Field                            | Type   | Description                     | Example       |
| -------------------------------- | ------ | ------------------------------- | ------------- |
| `test_plan_id`                   | string | Echo of the plan triggered      | `ADuF4ViN`    |
| `execution_details.status`       | string | Execution state at trigger time | `triggered`   |
| `execution_details.execution_id` | string | Identifier for this execution   | `exec_123456` |

`status: triggered` means Drizz accepted the request and started provisioning. It carries no information about whether the tests pass.

## Trigger multiple test plans

```
POST <DRIZZ_API_BASE_URL>/testplan/run/batch
```

One batch request replaces a loop of single triggers, stays inside the rate limit, and provisions devices independently for each plan.

```bash
curl -X POST "$DRIZZ_BASE_URL/testplan/run/batch" \
  -H "x-api-key: $DRIZZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "test_plans": [
      {
        "test_plan_id": "ADuF4ViN",
        "apks": { "com.shopease.android": "1.0.0" }
      },
      {
        "test_plan_id": "nBZfQL3R",
        "apks": { "com.shopease.android": "1.0.0" }
      }
    ]
  }'
```

### Request parameters

| Name                        | Type   | Required | Max | Description                                                            |
| --------------------------- | ------ | -------- | --- | ---------------------------------------------------------------------- |
| `test_plans`                | array  | Yes      | —   | List of test plan objects                                              |
| `test_plans[].test_plan_id` | string | Yes      | 64  | Test plan ID. Example: `nBZfQL3R`                                      |
| `test_plans[].apks`         | object | Yes      | —   | Package-to-version map. Example: `{ "com.shopease.android": "1.0.0" }` |

### Response

| Field                   | Type   | Description                                         | Example                  |
| ----------------------- | ------ | --------------------------------------------------- | ------------------------ |
| `message`               | string | Batch status                                        | `Triggered successfully` |
| `successful_executions` | array  | Plans that started, with their execution references | `[]`                     |
| `failed_executions`     | array  | Plans that did not start, with the reason           | `[]`                     |

A batch can partially succeed. **HTTP 207** means some plans started and some did not. Read `failed_executions` rather than treating any 2xx as a clean start.

## Errors

| Code    | Meaning           | What to do                                                      |
| ------- | ----------------- | --------------------------------------------------------------- |
| **200** | Success           | Record `execution_id`                                           |
| **207** | Multi-Status      | Batch partially succeeded — inspect `failed_executions`         |
| **400** | Bad Request       | Validate the payload and parameters                             |
| **404** | Not Found         | Verify the test plan ID and that the app version is registered  |
| **429** | Too Many Requests | Over the rate limit. Use the batch endpoint, back off and retry |
| **500** | Server Error      | Contact `support@drizz.dev`                                     |
| **502** | Bad Gateway       | Retry, or check service availability                            |

## Where the API ends

{% hint style="warning" %}
**There is no endpoint to poll a run or fetch its results.** Once you have an `execution_id`, the API has nothing more to give you: no status lookup, no report, no artifacts, no JUnit or JSON export, no webhook on completion.

Results live in the Drizz web app. Read the report there. If you're building a pipeline around this, read [CI/CD](/automate-and-integrate/ci-cd) first — it changes what you can sensibly build.
{% endhint %}

## Common mistakes

| What you did                                   | What happens                                          |
| ---------------------------------------------- | ----------------------------------------------------- |
| Treated `status: triggered` as "tests passed"  | It only means the run started                         |
| Polled a status endpoint                       | There isn't one. The call 404s                        |
| Looped single triggers for a big suite         | HTTP 429. Use the batch endpoint                      |
| Named a version you never uploaded             | HTTP 404 — the app version isn't registered           |
| Read a 207 as full success                     | Some plans failed to start. Check `failed_executions` |
| Hard-coded a plan ID from another organization | 404 — plan IDs don't cross organizations              |

## Next

* [Errors & limits](/automate-and-integrate/errors-and-limits) — status codes, rate limits, retries
* [CI/CD](/automate-and-integrate/ci-cd) — what a pipeline can and can't do today
* [Test plans](/running-tests/test-plans) — where plan IDs come from

***

*Last updated: 6 August 2026*


# Errors & limits

Every status code the Drizz API returns, what each common failure means, and the rate limits to design around.

Status codes, failure causes and rate limits for the Drizz API.

|                    |                                                                     |
| ------------------ | ------------------------------------------------------------------- |
| **Rate limit**     | \~4 requests/second, burst \~20, per client                         |
| **Over the limit** | HTTP **429** — back off and retry                                   |
| **Token life**     | 24 hours                                                            |
| **Upload cap**     | 500 MB per binary                                                   |
| **Watch out**      | **207** on a batch means partial success — read `failed_executions` |

## Prerequisites

None.

## Status codes

<table data-search="false"><thead><tr><th>Code</th><th>Meaning</th><th>What to do</th></tr></thead><tbody><tr><td><strong>200</strong></td><td>Success</td><td>Execution triggered successfully</td></tr><tr><td><strong>207</strong></td><td>Multi-Status</td><td>Batch partially succeeded — inspect <code>failed_executions</code></td></tr><tr><td><strong>400</strong></td><td>Bad Request</td><td>Validate your payload and parameters</td></tr><tr><td><strong>404</strong></td><td>Not Found</td><td>Verify the test plan ID and that the app version is registered</td></tr><tr><td><strong>409</strong></td><td>Conflict</td><td>This app version already exists — bump the version or skip the upload</td></tr><tr><td><strong>429</strong></td><td>Too Many Requests</td><td>You're over the rate limit. Back off and retry</td></tr><tr><td><strong>500</strong></td><td>Server Error</td><td>Contact Drizz support</td></tr><tr><td><strong>502</strong></td><td>Bad Gateway</td><td>Retry, or check service availability</td></tr></tbody></table>

## Common failures

### App with package name not found

| Cause                             | Fix                                                            |
| --------------------------------- | -------------------------------------------------------------- |
| The app isn't registered in Drizz | Register it — see [Managing apps](/your-account/managing-apps) |
| Package name mismatch             | Check the exact package name, including case                   |
| Organization access issue         | The app belongs to a different organization                    |

### Test plan not found

| Cause                            | Fix                                      |
| -------------------------------- | ---------------------------------------- |
| Invalid test plan ID             | Confirm the ID in the web app            |
| The plan was archived or deleted | Restore it, or create a new plan         |
| Cross-organization access        | Plan IDs don't work across organizations |

### File size exceeded

| Cause                 | Fix                                                         |
| --------------------- | ----------------------------------------------------------- |
| Binary is over 500 MB | Strip debug symbols, drop unused resources, or split by ABI |

### Invalid access token

| Cause                                   | Fix                                                        |
| --------------------------------------- | ---------------------------------------------------------- |
| Token expired — they last 24 hours      | Request a fresh token at the start of each run             |
| Wrong client credentials                | Check the client ID, secret and audience                   |
| Missing or malformed `x-api-key` header | Send the token as `x-api-key`, not `Authorization: Bearer` |

## Rate limits

| Limit           | Value                               |
| --------------- | ----------------------------------- |
| Sustained rate  | \~4 requests per second, per client |
| Burst allowance | \~20 requests                       |
| Over the limit  | HTTP 429                            |

## Retry behavior

| Code    | Retry | Detail                                                                              |
| ------- | ----- | ----------------------------------------------------------------------------------- |
| **429** | Yes   | Exponential backoff with jitter. A tight retry loop extends the rate-limited window |
| **502** | Yes   | Transient. Retry with backoff                                                       |
| **500** | No    | Contact `support@drizz.dev`                                                         |
| **400** | No    | Fails identically every time. Fix the request                                       |
| **404** | No    | Fix the test plan ID or register the app version                                    |
| **409** | No    | The version exists. Bump the version or skip the upload                             |

## Request budget

| Rule                                                           | Reason                                                                                  |
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| One auth call per pipeline run, not per API call               | Repeated auth calls consume the rate limit                                              |
| One `POST /testplan/run/batch` instead of one trigger per plan | Fewer requests, better parallelism                                                      |
| Upload only when the app version changes                       | Re-uploading a registered version returns 409 and transfers up to 500 MB with no effect |
| Log the raw request and response in CI                         | The trigger response is the only machine-readable record of a run                       |
| Read `failed_executions` on every batch                        | A 207 is a partial start, not a clean run                                               |

## Common mistakes

| What you did                          | What happens                                        |
| ------------------------------------- | --------------------------------------------------- |
| Retried a 400 five times              | Same failure five times. Fix the payload            |
| Tight retry loop after a 429          | You stay rate limited longer                        |
| Treated any 2xx from batch as success | 207 hides failed plans                              |
| Requested a token per API call        | You'll hit the rate limit on auth alone             |
| Assumed 404 means the API is down     | It means a wrong plan ID or an unregistered version |
| Discarded the API response in CI      | Nothing to debug with when it fails                 |

## Support

For API access and issues, contact `support@drizz.dev`.

## Next

* [API overview](/automate-and-integrate/api-overview) — the auth model and the three-call flow
* [Trigger a run](/automate-and-integrate/trigger-a-run) — where 207 and 404 come from
* [CI/CD](/automate-and-integrate/ci-cd) — what a pipeline can enforce today

***

*Last updated: 6 August 2026*


# CI/CD

Trigger Drizz test plans from GitHub Actions, Jenkins, GitLab CI and others — and what a pipeline cannot do yet.

Any CI/CD system that can make an authenticated HTTP request can start a Drizz run: authenticate, upload the build, trigger the plan.

|                   |                                                                           |
| ----------------- | ------------------------------------------------------------------------- |
| **Platforms**     | GitHub Actions · Jenkins · GitLab CI · Bitbucket Pipelines · Azure DevOps |
| **Requirement**   | Anything that can make an authenticated HTTP request                      |
| **Env vars**      | `DRIZZ_BASE_URL`, `DRIZZ_API_KEY`                                         |
| **Pipeline does** | authenticate → upload → trigger                                           |
| **Watch out**     | The trigger step succeeds even if every test later fails                  |

## Prerequisites

* Client ID, client secret, auth host and audience, issued by Drizz
* API base URL for your organization
* Test plan ID, from the web app
* A CI secret store holding all five values
* `curl` and `jq` available on the build agent

## What a pipeline can and cannot do

| Consequence                                                     | Detail                                       |
| --------------------------------------------------------------- | -------------------------------------------- |
| The trigger step goes green as soon as the run is **triggered** | It reports the trigger, not the test outcome |
| A build cannot be failed on a Drizz test failure                | No run status is available to the pipeline   |
| Results are read by a person                                    | Open the report in the Drizz web app         |

## Copy this

```bash
#!/usr/bin/env bash
set -euo pipefail

# Secrets come from your CI secret store, never from the repository
: "${DRIZZ_BASE_URL:?}" "${DRIZZ_AUTH_DOMAIN:?}" "${DRIZZ_CLIENT_ID:?}"
: "${DRIZZ_CLIENT_SECRET:?}" "${DRIZZ_AUDIENCE:?}"

# 1 — fresh token for this run
DRIZZ_API_KEY=$(curl -sf -X POST "https://${DRIZZ_AUTH_DOMAIN}/oauth/token" \
  -H "Content-Type: application/json" \
  -d "{\"client_id\":\"${DRIZZ_CLIENT_ID}\",\"client_secret\":\"${DRIZZ_CLIENT_SECRET}\",\"audience\":\"${DRIZZ_AUDIENCE}\",\"grant_type\":\"client_credentials\"}" \
  | jq -r .access_token)

# 2 — upload the build
curl -sf -X POST "${DRIZZ_BASE_URL}/apk/upload" \
  -H "x-api-key: ${DRIZZ_API_KEY}" \
  -F "file=@app-release.apk" | tee upload.json

# 3 — trigger the plan against the version just uploaded
VERSION=$(jq -r .details.version_name upload.json)
curl -sf -X POST "${DRIZZ_BASE_URL}/testplan/run" \
  -H "x-api-key: ${DRIZZ_API_KEY}" \
  -H "Content-Type: application/json" \
  -d "{\"test_plan_id\":\"ADuF4ViN\",\"apks\":{\"com.shopease.android\":\"${VERSION}\"}}" \
  | tee trigger.json

# The run has started. Open the report in the Drizz web app for the outcome.
echo "execution_id: $(jq -r .execution_details.execution_id trigger.json)"
```

## Pipeline steps

1. Request an access token.
2. Upload the build — only when the app version changed.
3. Trigger one test plan, or a batch.
4. Record the `execution_id` in the build log.
5. Archive the trigger response as a build artifact.
6. Open the report in the Drizz web app to read the outcome. This step is manual.

## Supported platforms

The integration is identical on every platform. Only the surrounding syntax changes.

| Platform                | Trigger points                         |
| ----------------------- | -------------------------------------- |
| **GitHub Actions**      | Pull requests, merges, manual dispatch |
| **Jenkins**             | Scripted or declarative pipelines      |
| **GitLab CI**           | Branch pipelines, scheduled jobs       |
| **Bitbucket Pipelines** | Build or release stages                |
| **Azure DevOps**        | Release pipelines                      |

## Platform examples

Both examples trigger a run and finish. Each passes whether the tests pass or fail. The step that would wait for the result is marked in the file and deliberately not written, because there is no endpoint to call.

{% tabs %}
{% tab title="GitHub Actions" %}

```yaml
name: Drizz regression

on:
  workflow_dispatch:
  push:
    branches: [main]

jobs:
  drizz:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build the app
        run: ./gradlew assembleRelease

      - name: Authenticate with Drizz
        id: auth
        env:
          DRIZZ_AUTH_DOMAIN: ${{ secrets.DRIZZ_AUTH_DOMAIN }}
          DRIZZ_CLIENT_ID: ${{ secrets.DRIZZ_CLIENT_ID }}
          DRIZZ_CLIENT_SECRET: ${{ secrets.DRIZZ_CLIENT_SECRET }}
          DRIZZ_AUDIENCE: ${{ secrets.DRIZZ_AUDIENCE }}
        run: |
          TOKEN=$(curl -sf -X POST "https://${DRIZZ_AUTH_DOMAIN}/oauth/token" \
            -H "Content-Type: application/json" \
            -d "{\"client_id\":\"${DRIZZ_CLIENT_ID}\",\"client_secret\":\"${DRIZZ_CLIENT_SECRET}\",\"audience\":\"${DRIZZ_AUDIENCE}\",\"grant_type\":\"client_credentials\"}" \
            | jq -r .access_token)
          echo "::add-mask::$TOKEN"
          echo "token=$TOKEN" >> "$GITHUB_OUTPUT"

      - name: Upload the build
        env:
          DRIZZ_BASE_URL: ${{ secrets.DRIZZ_BASE_URL }}
          DRIZZ_API_KEY: ${{ steps.auth.outputs.token }}
        run: |
          curl -sf -X POST "${DRIZZ_BASE_URL}/apk/upload" \
            -H "x-api-key: ${DRIZZ_API_KEY}" \
            -F "file=@app/build/outputs/apk/release/app-release.apk" \
            | tee upload.json

      - name: Trigger the test plan
        env:
          DRIZZ_BASE_URL: ${{ secrets.DRIZZ_BASE_URL }}
          DRIZZ_API_KEY: ${{ steps.auth.outputs.token }}
        run: |
          VERSION=$(jq -r .details.version_name upload.json)
          curl -sf -X POST "${DRIZZ_BASE_URL}/testplan/run" \
            -H "x-api-key: ${DRIZZ_API_KEY}" \
            -H "Content-Type: application/json" \
            -d "{\"test_plan_id\":\"ADuF4ViN\",\"apks\":{\"com.shopease.android\":\"${VERSION}\"}}" \
            | tee trigger.json
          echo "Started: $(jq -r .execution_details.execution_id trigger.json)"

      # BLOCKED — a "wait for the run and fail on failure" step goes here.
      # It needs a run-status endpoint, which does not exist yet.
      # Until then this workflow reports that a run started, not whether it passed.

      - name: Keep the trigger record
        uses: actions/upload-artifact@v4
        with:
          name: drizz-trigger
          path: |
            upload.json
            trigger.json
```

{% endtab %}

{% tab title="Jenkins" %}

```groovy
pipeline {
  agent any

  environment {
    DRIZZ_BASE_URL   = credentials('drizz-base-url')
    DRIZZ_AUTH       = credentials('drizz-auth-domain')
    DRIZZ_AUDIENCE   = credentials('drizz-audience')
    DRIZZ_CLIENT     = credentials('drizz-client-id')
    DRIZZ_SECRET     = credentials('drizz-client-secret')
    TEST_PLAN_ID     = 'ADuF4ViN'
    PACKAGE_NAME     = 'com.shopease.android'
  }

  stages {
    stage('Build') {
      steps {
        sh './gradlew assembleRelease'
      }
    }

    stage('Authenticate') {
      steps {
        script {
          env.DRIZZ_API_KEY = sh(
            returnStdout: true,
            script: '''
              curl -sf -X POST "https://${DRIZZ_AUTH}/oauth/token" \
                -H "Content-Type: application/json" \
                -d "{\\"client_id\\":\\"${DRIZZ_CLIENT}\\",\\"client_secret\\":\\"${DRIZZ_SECRET}\\",\\"audience\\":\\"${DRIZZ_AUDIENCE}\\",\\"grant_type\\":\\"client_credentials\\"}" \
                | jq -r .access_token
            '''
          ).trim()
        }
      }
    }

    stage('Upload') {
      steps {
        sh '''
          curl -sf -X POST "${DRIZZ_BASE_URL}/apk/upload" \
            -H "x-api-key: ${DRIZZ_API_KEY}" \
            -F "file=@app/build/outputs/apk/release/app-release.apk" \
            | tee upload.json
        '''
      }
    }

    stage('Trigger') {
      steps {
        sh '''
          VERSION=$(jq -r .details.version_name upload.json)
          curl -sf -X POST "${DRIZZ_BASE_URL}/testplan/run" \
            -H "x-api-key: ${DRIZZ_API_KEY}" \
            -H "Content-Type: application/json" \
            -d "{\\"test_plan_id\\":\\"${TEST_PLAN_ID}\\",\\"apks\\":{\\"${PACKAGE_NAME}\\":\\"${VERSION}\\"}}" \
            | tee trigger.json
        '''
      }
    }

    // BLOCKED — a "Wait for result" stage goes here once a run-status
    // endpoint exists. There is nothing to poll today.
  }

  post {
    always {
      archiveArtifacts artifacts: 'upload.json,trigger.json', allowEmptyArchive: true
      echo 'Drizz run triggered. Open the report in the Drizz web app for the outcome.'
    }
  }
}
```

{% endtab %}
{% endtabs %}

## Secrets

| Value                                                   | Where it belongs                                                       |
| ------------------------------------------------------- | ---------------------------------------------------------------------- |
| Client ID, client secret, audience, auth host, base URL | CI secret store. Never committed                                       |
| Access token                                            | Run-scoped variable, masked in logs — `::add-mask::` in GitHub Actions |

## Common mistakes

| What you did                                     | What happens                                   |
| ------------------------------------------------ | ---------------------------------------------- |
| Treated a green trigger step as a green test run | It only means the run started                  |
| Put the Drizz step in front of a release gate    | The gate lets everything through               |
| Uploaded the build on every commit               | Slow pipelines, and 409s on unchanged versions |
| Cached the token between pipeline runs           | It expires after 24 hours                      |
| Echoed the token in a build log                  | Anyone with log access can trigger your runs   |
| Looped single triggers over 30 plans             | HTTP 429. Use the batch endpoint               |

## Next

* [API overview](/automate-and-integrate/api-overview) — the base URL and the auth model
* [Trigger a run](/automate-and-integrate/trigger-a-run) — single and batch triggers
* [Errors & limits](/automate-and-integrate/errors-and-limits) — rate limits and retries

***

*Last updated: 6 August 2026*


# Jira

Create Jira issues from failed Drizz tests, and the shared-token attribution caveat that applies to every generated ticket.

Drizz creates a Jira issue from a failed test.

|                    |                                                                                            |
| ------------------ | ------------------------------------------------------------------------------------------ |
| **What it does**   | Creates a Jira issue when a test fails                                                     |
| **Authentication** | A single shared token for the whole organization                                           |
| **You can set**    | The assignee                                                                               |
| **You can't set**  | The reporter                                                                               |
| **Watch out**      | Every ticket is reported by whoever connected the integration, not by whoever ran the test |

## Prerequisites

* Drizz organization account
* A Jira project to file issues into
* A Jira account whose token authenticates the integration — a service account, not a personal account
* Setup details from your Drizz contact

## Attribution

The integration authenticates with **one shared token**. Every issue is attributed to the account that connected the integration, regardless of who triggered the run.

| Field    | Configurable | Value on a generated ticket                |
| -------- | ------------ | ------------------------------------------ |
| Assignee | Yes          | Whoever you configure                      |
| Reporter | No           | The account that connected the integration |

Consequences:

| Constraint                                        | What to do                                                                               |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| The reporter is identical on every ticket         | Do not key filters, dashboards or automation on it                                       |
| Run context is not carried into any Jira field    | Put the triggering user, the test plan and the report link in the summary or description |
| A personal account becomes the permanent reporter | Connect with a service account                                                           |

## Common mistakes

| What you did                                      | What happens                                              |
| ------------------------------------------------- | --------------------------------------------------------- |
| Filtered Jira by reporter to find your failures   | Every Drizz ticket has the same reporter                  |
| Connected the integration with a personal account | Every generated ticket is attributed to you, indefinitely |
| Expected the triggering user's name on the ticket | It isn't carried through. Put it in the description       |
| Assumed one ticket per unique failure             | Deduplication behavior isn't confirmed                    |

## Next

* [When a step fails](/reports-and-debugging/when-a-step-fails) — triaging before you file
* [Statuses](/reports-and-debugging/statuses) — what counts as a failure
* [CI/CD](/automate-and-integrate/ci-cd) — triggering runs from a pipeline

***

*Last updated: 6 August 2026*


# Glossary

Every Drizz term in one place — DT, Memory, modules, datasets, lockable pools, MAP\_ACTION, Vision AI and the rest.

The vocabulary used across these docs and in the product UI, alphabetically.

|               |                                                                   |
| ------------- | ----------------------------------------------------------------- |
| **Covers**    | Terms used throughout the documentation and in the product UI     |
| **Naming**    | Chat is also referred to as Fathom in older material              |
| **Watch out** | "Memory" names two different things. Check which one a page means |

<table data-search="false"><thead><tr><th>Term</th><th>Definition</th></tr></thead><tbody><tr><td><strong>AUT</strong></td><td>App under test — the app Drizz is driving</td></tr><tr><td><strong>Blocker rule</strong></td><td>A Memory rule that dismisses a known popup automatically. One rule carries one action</td></tr><tr><td><strong>Cached step</strong></td><td>A step Drizz has resolved on this screen before and replays without calling Vision AI. Lowest-cost step category</td></tr><tr><td><strong>Chat</strong></td><td>The panel that generates a test from a plain-English description. Referred to as <strong>Fathom</strong> in older material</td></tr><tr><td><strong>Dataset</strong></td><td>Named values bound to a test plan at run time and referenced in a script as <code>{{var}}</code>. Nesting is limited to two levels</td></tr><tr><td><strong>Desktop app</strong></td><td>The Drizz application installed locally to author tests and run them on a connected device</td></tr><tr><td><strong>Drizz device cloud</strong></td><td>The hosted devices a test plan runs on. One of three run targets, with local and private devices</td></tr><tr><td><strong>DT</strong></td><td>Drizz Token — the unit Drizz meters usage in, deducted per step. Current rates are on your organization's billing page</td></tr><tr><td><strong><code>execution_id</code></strong></td><td>The identifier returned when a test plan is triggered over the API. Confirms a run started. Not pollable</td></tr><tr><td><strong>Lockable pool</strong></td><td>A dataset whose rows are leased exclusively to one run at a time. A run waits when the pool is fully leased</td></tr><tr><td><strong><code>MAP_ACTION</code></strong></td><td>A grid- and coordinate-based gesture, for elements Vision AI cannot target normally. Pinch and zoom are limited to specific Android emulator types</td></tr><tr><td><strong>Memory</strong></td><td>Org-level app context and blocker rules, applied before a run starts. Unrelated to <code>Store</code> and <code>SET</code> variables</td></tr><tr><td><strong>Module</strong></td><td>A reusable block of steps, defined once and invoked with <code>CALL</code>. Takes parameters, passed by value</td></tr><tr><td><strong>Private device</strong></td><td>A device reserved for a single organization</td></tr><tr><td><strong>Registered app</strong></td><td>An app binary uploaded to Drizz, available to test plans and to Memory. Capped at 500 MB</td></tr><tr><td><strong>Self-healing</strong></td><td>Automatic repair of a failed step, a limited number of times per run. Healed steps are badged in the report</td></tr><tr><td><strong><code>SET</code></strong></td><td>Assigns a value to a variable inside a script</td></tr><tr><td><strong><code>Store</code></strong></td><td>Captures a value from the current screen into a variable</td></tr><tr><td><strong>Test plan</strong></td><td>A set of tests plus how and where they run — devices, concurrency, app version, dataset binding</td></tr><tr><td><strong>Variable</strong></td><td>A named value used inside a script. Produced by <code>Store</code>, <code>SET</code>, dataset values or placeholders</td></tr><tr><td><strong>Vision AI</strong></td><td>The layer that reads the screen and locates elements without selectors or an SDK</td></tr><tr><td><strong>Wallet</strong></td><td>An organization's DT balance, shared org-wide. States are Healthy, Low and Blocked</td></tr></tbody></table>

## Terms with two names

| Term   | Also called | Rule in these docs                                                                                                      |
| ------ | ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| Chat   | Fathom      | Write **Chat**. Fathom in older material means Chat                                                                     |
| Memory | —           | **Memory** is the org-level app context and blocker rules only. Runtime values from `Store` and `SET` are **variables** |

## Next

* [Command index](/writing-tests/command-index) — every command, one table
* [Known limitations](/reference/known-limitations) — what Drizz doesn't do
* [Which variable do I use?](/writing-tests/which-variable) — Store vs SET vs dataset vs placeholder

***

*Last updated: 6 August 2026*


# Known Limitations

What Drizz doesn't do — platform gaps, language limits, Chat's scope, the accessibility boundary, and what's missing from the API.

Every documented limitation, grouped by where it applies. This page is the single source of truth for limits.

|                   |                                                                               |
| ----------------- | ----------------------------------------------------------------------------- |
| **Platform**      | `PRESS_DEVICE_BACK_BUTTON` doesn't exist on iOS; `CLEAR_APP` is a no-op there |
| **Language**      | No loops; a blocker rule carries one action; datasets nest two levels         |
| **Accessibility** | Visual checks only — no WCAG, screen reader, contrast or ARIA validation      |
| **Automation**    | No run-status API, no JUnit/JSON export, no webhooks                          |
| **Watch out**     | Secure screens (payment PIN entry) render black and can't be automated at all |

## Platform

| Limitation                                                                       | What to do instead                                         |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `PRESS_DEVICE_BACK_BUTTON` doesn't exist on iOS                                  | Use `Tap on the back button`, or a left-edge swipe         |
| `CLEAR_APP` is effectively a no-op on iOS                                        | Reinstall the build, or reset state in the app itself      |
| Pinch and zoom gestures are limited to specific Android emulator types — not iOS | Avoid pinch-dependent assertions in cross-platform tests   |
| iOS steps run roughly **twice as slow** as the equivalent Android steps          | Budget wall-clock time accordingly; keep iOS plans smaller |

## Language

| Limitation                                   | What to do instead                                             |
| -------------------------------------------- | -------------------------------------------------------------- |
| **No loops.** There's no iteration construct | Repeat the steps, or use a data-driven test plan               |
| A **blocker rule carries one action only**   | Use one rule per action, or handle the flow with an `IF` block |
| **Dataset nesting is limited to two levels** | Flatten the structure before you bind it                       |

## Chat

| Limitation                                                            | What to do instead                      |
| --------------------------------------------------------------------- | --------------------------------------- |
| **Android only** — Chat doesn't generate tests against iOS            | Author iOS tests by hand                |
| **Roughly 20–25 steps** per generated test                            | Split longer flows, or hand-author them |
| **No API steps, module calls, `Store` or `SET`** in generated scripts | Add these by hand after generation      |

Chat is also referred to as Fathom in older material — same feature.

## Accessibility

Drizz validates what is visually rendered on screen. Everything below that boundary is out of scope.

| Not supported                          | Detail                                                                                             |
| -------------------------------------- | -------------------------------------------------------------------------------------------------- |
| **WCAG conformance scanning**          | No compliance report. Nothing Drizz produces is evidence of legal conformance                      |
| **Screen reader testing**              | No TalkBack, VoiceOver or NVDA. Drizz does not drive assistive technology                          |
| **Contrast-ratio and color analysis**  | No contrast checks, color-blindness simulation or palette validation                               |
| **Semantic and role-level validation** | No inspection of ARIA roles, accessibility labels, alt text, focus order or the accessibility tree |

A dedicated accessibility tool is required for any of the above.

## Automation

| Missing                                        | Consequence                                                          |
| ---------------------------------------------- | -------------------------------------------------------------------- |
| **No API to poll run status or fetch results** | A pipeline can start a run but can't wait for it                     |
| **No JUnit, JSON or Allure export**            | Results can't be ingested by a CI reporter or a test-management tool |
| **No webhooks**                                | Nothing calls you back when a run finishes                           |

CI integration is fire-and-forget: a pipeline can trigger Drizz and cannot gate a build on the outcome. See [CI/CD](/automate-and-integrate/ci-cd).

## Environment

| Limitation                                                 | Detail                                                                                                                                                         |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Secure screens can't be automated**                      | Payment PIN entry and some banking flows render black to any capture tool, Drizz included. There is no workaround — the screen contents don't exist to capture |
| **Location can be set at provisioning, not mid-test**      | Set it when the cloud device is provisioned. You can't change it partway through a run                                                                         |
| **Precise-location permission isn't grantable everywhere** | Availability varies by device target                                                                                                                           |

## Next

* [Changelog](/reference/changelog) — limitations lifted, newest first
* [Glossary](/reference/glossary) — terms used above
* [Troubleshooting](/reports-and-debugging/troubleshooting) — symptom, then fix

***

*Last updated: 6 August 2026*


# Changelog

User-visible changes to Drizz, newest first — new features, changed behavior, and anything affecting tests already written.

User-visible changes to Drizz, newest first.

|                |                                                                                           |
| -------------- | ----------------------------------------------------------------------------------------- |
| **Order**      | Reverse chronological — newest entry at the top                                           |
| **Covers**     | User-visible changes to the desktop app, the web app and the device cloud                 |
| **Grouped by** | Added · Changed · Fixed                                                                   |
| **Watch out**  | Entries are dated by release. On an older desktop build, update before expecting a change |

## Scope of an entry

| Rule                        | Detail                                                                   |
| --------------------------- | ------------------------------------------------------------------------ |
| One entry per release month | Newest first                                                             |
| Included                    | A new command, a changed default, a new device type, a limitation lifted |
| Excluded                    | Internal work, infrastructure, anything unreleased                       |
| Test-affecting changes      | Listed under **Changed**, with the action to take                        |

***

## August 2026

**Added**

* **Parameterized modules.** Modules take parameters, so one module covers several variations. Values are passed by value at the call site. See [Modules](/writing-tests/modules).
* **Datasets and variables.** Test data can be defined as a dataset and bound to a test plan at run time, referenced in scripts as `{{var}}`. Nesting is limited to two levels. Lockable pools lease a row exclusively to one run, so parallel runs never collide on the same data. See [Datasets](/writing-tests/which-variable/datasets) and [Lockable pools](/writing-tests/which-variable/lockable-pools).
* **Physical iOS device support.** Tests run on a real iPhone or iPad connected to your Mac, in addition to the simulator. Requires Developer Mode on the device and a signing team. Test plans require a real-device build. See [iOS physical device](/desktop-app/ios-physical-device).
* **Accessibility checks in reports.** Runs surface accessibility observations alongside the standard report. Visual checks only — no WCAG conformance scanning, screen reader testing, contrast analysis or ARIA validation. See [Known limitations](/reference/known-limitations#accessibility).
* **Private devices.** Devices reserved for a single organization, alongside local and cloud devices. Ask your Drizz contact about availability. See [Devices](/running-tests/devices).

***

## Next

* [Known limitations](/reference/known-limitations) — what's still not possible
* [Glossary](/reference/glossary) — terms used in these entries
* [Command index](/writing-tests/command-index) — every command, one table

***

*Last updated: 6 August 2026*


