Statement Types
| 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 page, Function on the Custom Functions page, and Template on the Templates page.
VERIFY
Asserts a condition using AI. Use the VERIFY: shorthand (unquoted key):
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:
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.
For complex interactions that don't map to a supported action (e.g., drag-and-drop), use the description: + js: code step, but note raw JS does not self-heal:
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.
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 fieldFrames
For elements inside iframes, use frame_path with action: form:
- intent: Click Hello inside iframe
action: click
frame_path:
- "iframe#main"
locator: "getByText('Hello')"Conditional Logic
Handle optional UI elements with IF/ELSE:
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 submitConditions are evaluated by the AI at runtime using the current page state. JavaScript conditions are also supported with the js: prefix:
- IF: "js: testContext.retryCount < 3"
THEN:
- intent: Click the retry buttonWARNING
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:
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 loadedJavaScript conditions work in loops too (same js: caveat applies):
- 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:
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: 10When 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.
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:
- WAIT: Wait for the open animation
seconds: 2