---
title: "Statement Types"
description: "Every kind of step in a YAML E2E test: ACTION, VERIFY, DRAFT, URL, STEP grouping, frames, conditional logic (IF/ELSE), and loops (WHILE)."
---

# Statement Types

<div class="view-markdown-wrapper">
<ViewMarkdown />
</div>

| Type               | Syntax                                              | Description                                                          |
| ------------------ | --------------------------------------------------- | -------------------------------------------------------------------- |
| ACTION (`action:`) | `- intent: Enter email` + `action: input_text`      | Fast replay with AI self-healing fallback                            |
| ACTION (`js:`)     | `- intent: Click login` + `js: "await ..."`         | Fast replay (Playwright code) with AI self-healing                   |
| VERIFY             | `- VERIFY: page shows welcome message`              | AI assertion, optional `js:` cache                                   |
| DRAFT              | `- intent: Click the login button`                  | AI resolves at runtime (~5-10s)                                      |
| URL                | `- URL: /path`                                      | Navigation shorthand                                                 |
| Code               | `- description: ...` + `js: await request.get(...)` | Inline Playwright code (no self-healing)                             |
| STEP               | `- STEP: Login` + `statements: [...]`               | Group related actions                                                |
| IF/ELSE            | `- IF: cookie banner is visible` + `THEN: [...]`    | Conditional execution                                                |
| WHILE              | `- WHILE: more items to load` + `DO: [...]`         | Repeat until condition                                               |
| WAIT_UNTIL         | `- WAIT_UNTIL: dashboard loaded` + optional `js:`   | Poll until condition met (AI, or a `js:` predicate — no model calls) |
| WAIT               | `- WAIT: for animation` + `seconds: 3`              | Fixed-duration pause                                                 |
| Function           | `- call: "file#export"` + `args: [...]`             | Call custom TypeScript function                                      |
| Template           | `- template: ./path.yaml`                           | Inline reusable statement flow                                       |

The `Code` type is covered in detail on the [Code Steps](/local/yaml-tests/code-step) page, `Function` on the [Custom Functions](/local/yaml-tests/custom-functions) page, and `Template` on the [Templates](/local/yaml-tests/templates) page.

## VERIFY

Asserts a condition using AI. Use the `VERIFY:` shorthand (unquoted key):

```yaml
statements:
  - VERIFY: The success message is displayed
  - VERIFY: The order total is $49.99
    js: "await expect(page.getByTestId('order-total')).toHaveText('$49.99')"
```

The `js:` cache speeds up simple checks. If the `js:` assertion fails, it automatically falls back to AI verification using the natural language statement.

## ACTION

Fast deterministic replay (<1s) with AI self-healing fallback. Use the **structured `action:` form** for all supported actions:

```yaml
statements:
  - intent: Type email address
    action: input_text
    text: "{{USER_EMAIL}}"
    locator: "getByLabel('Email')"
```

`intent` describes **what** the step should accomplish in natural language; the `action:`/`locator:` field is a cache for fast replay. When the cache fails (e.g., a locator becomes stale), Shiplight's agentic layer falls back to the intent to self-heal: see [How self-healing works](/guides/how-self-healing-works).

For complex interactions that don't map to a supported action (e.g., drag-and-drop), use the [`description: + js:` code step](/local/yaml-tests/code-step), but note raw JS does **not** self-heal:

```yaml
statements:
  - description: Drag the card to the Done column
    js: |
      const card = page.getByText('My Task');
      const target = page.getByTestId('column-done');
      await card.dragTo(target);
```

## STEP (grouping)

Groups related statements under a label.

```yaml
statements:
  - STEP: Fill in the registration form
    statements:
      - intent: Type "John" in the first name field
      - intent: Type "Doe" in the last name field
      - intent: Type "john@example.com" in the email field
```

## Frames

For elements inside iframes, use `frame_path` with `action:` form:

```yaml
- intent: Click Hello inside iframe
  action: click
  frame_path:
    - "iframe#main"
  locator: "getByText('Hello')"
```

## Conditional Logic

Handle optional UI elements with IF/ELSE:

```yaml
statements:
  - IF: cookie consent dialog is visible
    THEN:
      - intent: Click "Accept All"
  - IF: user is logged in
    THEN:
      - intent: Click the logout button
    ELSE:
      - intent: Click the login button
      - intent: Enter credentials and submit
```

Conditions are evaluated by the AI at runtime using the current page state. JavaScript conditions are also supported with the `js:` prefix:

```yaml
- IF: "js: testContext.retryCount < 3"
  THEN:
    - intent: Click the retry button
```

::: warning
`js:` conditions have no AI fallback: if the JavaScript throws, the test fails. They run in the Playwright test context (Node.js), so browser globals like `document`/`window` are **not** defined; a condition that references them throws. Use `js:` only for simple, reliable checks like URL matching or counters (`testContext.*`). For UI state checks, prefer natural language conditions — they self-heal when the DOM changes.
:::

## Loops

Repeat actions until a condition is met with WHILE:

```yaml
statements:
  - WHILE: "Load More" button is visible
    DO:
      - intent: Click the "Load More" button
      - intent: Wait for new items to appear
    timeout_ms: 30000
  - VERIFY: all items are loaded
```

JavaScript conditions work in loops too (same `js:` caveat applies):

```yaml
- WHILE: "js: testContext.itemCount < 10"
  DO:
    - intent: Click "Load More"
    - description: Increment the item counter
      js: "testContext.itemCount = (testContext.itemCount || 0) + 1"
```

## Waiting

Use `WAIT_UNTIL` to pause until a condition becomes true, then continue. The condition can be natural language (checked by the AI), or — preferred for simple DOM/state checks — a natural-language intent plus a `js:` expression polled in-process with **no model calls**:

```yaml
statements:
  # AI wait — semantic and self-healing, but each check is a model call
  - WAIT_UNTIL: Dashboard data has finished loading
    timeout_seconds: 15

  # Intent + js: wait — polled ~4x/second in-process, no model calls.
  # The intent labels the step in reports; the js: expression is what runs.
  - WAIT_UNTIL: The loading spinner has disappeared
    js: "(await page.locator('.spinner').count()) === 0"
    timeout_seconds: 10
```

When `js:` is present it is polled exclusively — there is no AI fallback (unlike VERIFY, where `js:` is a cache and the AI re-checks the assertion if it throws). The intent is what reports show, and what an agent uses to regenerate a stale expression. Prefer this form for simple checks (an element appearing or disappearing, a count reaching a threshold): it is far cheaper and faster than an AI wait, which costs a model call on every poll. Keep natural language alone for semantic conditions or when the selector may drift.

The expression runs in the Playwright test context (Node.js): `page`, `expect`, and `agent` are in scope and `await` is allowed, but browser globals like `document`/`window` are **not** — wrap browser-side checks in `page.evaluate(() => ...)`. The predicate must **return** truthy/falsy — a Playwright `waitFor()` resolves to `undefined` and never registers as met, so use a boolean check like the `count()` example above.

::: tip A wait never fails the test
`WAIT_UNTIL` synchronizes; it is not an assertion. When the timeout expires, the step records a warning and the test continues. If correctness depends on the condition, follow the wait with a `VERIFY:`.
:::

For a fixed pause, use `WAIT`, but prefer waiting on a condition whenever you can:

```yaml
- WAIT: Wait for the open animation
  seconds: 2
```
