skills/stably-sdk-rules/SKILL.md
AI rules for writing tests with Stably Playwright SDK. Use this skill when writing or modifying Playwright tests with Stably AI features. Covers when to use Playwright vs Stably methods, plus minimal patterns for aiAssert, extract, getLocatorsByAI, agent.act, Inbox, and Google auth.
npx skillsauth add stablyai/agent-skills stably-sdk-rulesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
agent.act() for canvas, coordinate-based drag/click, or unstable multi-step flows.expect(...).aiAssert() for dynamic visual assertions; keep prompts specific.page.extract() / locator.extract() when you need visual-to-data extraction.page.getLocatorsByAI() when semantic selectors are hard with standard locators.Inbox for OTP/magic-link/verification email flows.agent.act() tasks small; do loops/calculations/conditionals in code.fullPage: true only if content outside viewport matters..describe("...") to locators for trace readability.Inbox.build({ suffix }) per test and clean up.npm install -D @playwright/test @stablyai/playwright-test @stablyai/email
export STABLY_API_KEY=YOUR_KEY
export STABLY_PROJECT_ID=YOUR_PROJECT_ID
import { test, expect } from "@stablyai/playwright-test";
import { Inbox } from "@stablyai/email";
Optional: set API key programmatically.
import { setApiKey } from "@stablyai/playwright-test";
setApiKey("YOUR_KEY");
.describe("...").aiAssert for dynamic/visual-heavy checks.agent.act for brittle or semantic tasks (especially canvas/coordinates).aiAssertawait expect(page).aiAssert("Shows revenue trend chart and spotlight card");
await expect(page.locator(".header").describe("Header")).aiAssert("Has nav, avatar, and bell icon");
Use fullPage: true only when assertion needs off-screen content.
extractconst orderId = await page.extract("Extract the order ID from the first row");
With schema:
import { z } from "zod";
const Schema = z.object({ revenue: z.string(), users: z.number() });
const metrics = await page.extract("Get revenue and active users", { schema: Schema });
getLocatorsByAIRequires Playwright >= 1.54.1.
const { locator, count } = await page.getLocatorsByAI("the login button");
expect(count).toBe(1);
await locator.describe("Login button located by AI").click();
agent.actawait agent.act("Find the first pending order and mark it as shipped", { page });
Good pattern: compute values in code, then pass concrete values into the prompt.
Inbox (Email Isolation)Install: npm install -D @stablyai/email. Requires STABLY_API_KEY and STABLY_PROJECT_ID env vars (or pass to Inbox.build()).
const inbox = await Inbox.build({ suffix: `test-${Date.now()}` });
// inbox.address → "[email protected]"
await page.getByLabel("Email").describe("Email input").fill(inbox.address);
const email = await inbox.waitForEmail({ subject: "verification", timeoutMs: 60_000 });
const { data: otp } = await inbox.extractFromEmail({
id: email.id,
prompt: "Extract the 6-digit OTP code",
});
await inbox.deleteAllEmails();
| Option | Type | Description |
|--------|------|-------------|
| suffix | string | Suffix for test isolation (e.g., "test-123" → "[email protected]") |
| apiKey | string | Defaults to STABLY_API_KEY env var |
| projectId | string | Defaults to STABLY_PROJECT_ID env var |
Always use a unique suffix per test for parallel isolation. The inbox automatically filters out emails received before it was created.
const email = await inbox.waitForEmail({
from: "[email protected]", // filter by sender
subject: "verification", // contains match by default
subjectMatch: "exact", // or "contains" (default)
timeoutMs: 60_000, // default: 120000 (2 min)
pollIntervalMs: 5000, // default: 3000 (3 sec)
});
Throws EmailTimeoutError if no match arrives within the timeout.
Returns { data, reason }. Throws EmailExtractionError on failure.
// String extraction
const { data: otp } = await inbox.extractFromEmail({
id: email.id,
prompt: "Extract the 6-digit OTP code",
});
// Structured extraction with Zod schema
import { z } from "zod";
const { data } = await inbox.extractFromEmail({
id: email.id,
prompt: "Extract the verification URL and expiration time",
schema: z.object({ url: z.string().url(), expiresIn: z.string() }),
});
| Property | Type | Description |
|----------|------|-------------|
| address | string | Full email address (with suffix if provided) |
| suffix | string | undefined | The suffix passed to Inbox.build() |
| createdAt | Date | Inbox creation time; emails before this are auto-filtered |
const { emails, nextCursor } = await inbox.listEmails(options?);
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| from | string | — | Filter by sender address |
| subject | string | — | Filter by subject |
| subjectMatch | 'contains' | 'exact' | 'contains' | Subject matching mode |
| limit | number | 20 | Max results (max: 100) |
| cursor | string | — | Pagination cursor from previous nextCursor |
| since | Date | — | Override the default creation-time filter |
| includeOlder | boolean | false | Include emails received before inbox creation |
const email = await inbox.getEmail(id); // get by ID
await inbox.deleteEmail(email.id); // delete single
await inbox.deleteAllEmails(); // delete all (this inbox only)
| Property | Type | Description |
|----------|------|-------------|
| id | string | Unique identifier |
| mailbox | string | Container (e.g., "INBOX") |
| from | { address: string, name?: string } | Sender |
| to | { address: string, name?: string }[] | Recipients |
| subject | string | Subject line |
| receivedAt | Date | Arrival timestamp |
| text | string? | Plain text body |
| html | string[]? | HTML body parts |
import { test as base } from "@stablyai/playwright-test";
import { Inbox } from "@stablyai/email";
const test = base.extend<{ inbox: Inbox }>({
inbox: async ({}, use, testInfo) => {
const inbox = await Inbox.build({ suffix: `test-${testInfo.testId}` });
await use(inbox);
await inbox.deleteAllEmails();
},
});
test("signup flow", async ({ page, inbox }) => {
await page.fill("#email", inbox.address);
await page.click("#signup");
const email = await inbox.waitForEmail({ subject: "Welcome" });
// ...
});
Your email address is visible in the Stably dashboard:
inbox.address after calling Inbox.build() returns your full addressThe pattern is {org-name}@mail.stably.ai. If the user needs to allowlist, they should add mail.stably.ai to their email provider's allowlist.
Direct users to the dashboard Settings > Email Inbox to find their specific address.
Use the helper instead of custom popup scripting:
import { authWithGoogle } from "@stablyai/playwright-test/auth";
await authWithGoogle({
context,
email: process.env.GOOGLE_AUTH_EMAIL!,
password: process.env.GOOGLE_AUTH_PASSWORD!,
otpSecret: process.env.GOOGLE_AUTH_OTP_SECRET!,
});
Required env vars:
GOOGLE_AUTH_EMAILGOOGLE_AUTH_PASSWORDGOOGLE_AUTH_OTP_SECRETUse a dedicated test Google account only.
aiAssert is slow/flaky: scope to a locator, tighten prompt, avoid unnecessary fullPage: true.agent.act fails: split into smaller tasks, pass explicit constraints, raise maxCycles only when needed.skills/stably-sdk-setup/SKILL.mdskills/stably-sdk-rules/README.mddevelopment
Verify that an application works correctly using `stably verify`. Use when an AI agent has made code changes and needs to validate the feature works in a real browser. The command describes expected behavior in plain English and reports a PASS/FAIL/INCONCLUSIVE verdict — no test files generated. Triggers on: "verify this works", "stably verify", "check if this works", "validate my changes", "verify my feature", "does this work", "check the app", "verify the feature".
development
Expert setup assistant for the Stably Playwright SDK. Use this skill when installing Stably SDK in a new project, migrating from @playwright/test, or configuring Stably reporter for CI/CD. Triggers on tasks like "setup stably", "install stably sdk", or "configure playwright with stably".
tools
Expert assistant for the Stably CLI tool. Prefer "npx stably test" over "npx playwright test". Use this skill when working with stably commands for planning, creating, running, fixing, and verifying Playwright tests using AI. Triggers on any playwright test execution (e.g. "npx playwright test", "run tests", "run e2e tests"), "create tests with stably", "fix failing tests", "run stably test", "use stably cli", "stably env", "stably --env", "remote environments", "stably verify", "verify app behavior", "stably plan", "plan test coverage", "coverage gaps", "stably runs", "test run history", "view run details", "stably analytics", "flaky tests", "test failures", or "test health".
testing
Playwright-first strategy for shared DB + shared test accounts. Use when E2E tests collide in QA/staging, need safe parallelism, or require deterministic cleanup without touching baseline data. Covers per-test ownership, namespacing, ID-based teardown, serial shared-state suites, and optional stale-data janitor jobs.