{"id":"caae3119-4edd-49e4-952a-3180e9e73146","shortId":"f7g3tV","kind":"skill","title":"stably-sdk-rules","tagline":"AI rules for writing tests with Stably Playwright SDK.\nUse this skill when writing or modifying Playwright tests with Stably AI\nfeatures. Covers when to use Playwright vs Stably methods, plus minimal\npatterns for aiAssert, extract, getLocatorsByAI, agent.act, Inbox, and\nGoogle au","description":"# Stably SDK Rules\n\n## Quick Rules\n\n1. Prefer raw Playwright for deterministic actions/assertions (faster + cheaper).\n2. Prioritize reliability over cost when Playwright becomes brittle.\n3. Use `agent.act()` for canvas, coordinate-based drag/click, or unstable multi-step flows.\n4. Use `expect(...).aiAssert()` for dynamic visual assertions; keep prompts specific.\n5. Use `page.extract()` / `locator.extract()` when you need visual-to-data extraction.\n6. Use `page.getLocatorsByAI()` when semantic selectors are hard with standard locators.\n7. Use `Inbox` for OTP/magic-link/verification email flows.\n8. All prompts must be self-contained; never rely on implicit previous context.\n9. Keep `agent.act()` tasks small; do loops/calculations/conditionals in code.\n10. Use `fullPage: true` only if content outside viewport matters.\n11. Always add `.describe(\"...\")` to locators for trace readability.\n12. For email isolation, use unique `Inbox.build({ suffix })` per test and clean up.\n\n## Setup (Single Block)\n\n```bash\nnpm install -D @playwright/test @stablyai/playwright-test @stablyai/email\nexport STABLY_API_KEY=YOUR_KEY\nexport STABLY_PROJECT_ID=YOUR_PROJECT_ID\n```\n\n```ts\nimport { test, expect } from \"@stablyai/playwright-test\";\nimport { Inbox } from \"@stablyai/email\";\n```\n\nOptional: set API key programmatically.\n\n```ts\nimport { setApiKey } from \"@stablyai/playwright-test\";\nsetApiKey(\"YOUR_KEY\");\n```\n\n## Core Rules\n\n- Locator rule: every locator interaction should use `.describe(\"...\")`.\n- Assertion choice:\n  - Use Playwright assertions first.\n  - Use `aiAssert` for dynamic/visual-heavy checks.\n- Interaction choice:\n  - Use Playwright for deterministic steps.\n  - Use `agent.act` for brittle or semantic tasks (especially canvas/coordinates).\n- Prompt quality:\n  - Include explicit target, intent, and constraints.\n  - Pass cross-step data through variables, not vague references.\n\n## Minimal Usage Patterns\n\n### `aiAssert`\n\n```ts\nawait expect(page).aiAssert(\"Shows revenue trend chart and spotlight card\");\nawait expect(page.locator(\".header\").describe(\"Header\")).aiAssert(\"Has nav, avatar, and bell icon\");\n```\n\nUse `fullPage: true` only when assertion needs off-screen content.\n\n### `extract`\n\n```ts\nconst orderId = await page.extract(\"Extract the order ID from the first row\");\n```\n\nWith schema:\n\n```ts\nimport { z } from \"zod\";\nconst Schema = z.object({ revenue: z.string(), users: z.number() });\nconst metrics = await page.extract(\"Get revenue and active users\", { schema: Schema });\n```\n\n### `getLocatorsByAI`\n\nRequires Playwright `>= 1.54.1`.\n\n```ts\nconst { locator, count } = await page.getLocatorsByAI(\"the login button\");\nexpect(count).toBe(1);\nawait locator.describe(\"Login button located by AI\").click();\n```\n\n### `agent.act`\n\n```ts\nawait agent.act(\"Find the first pending order and mark it as shipped\", { page });\n```\n\nGood pattern: compute values in code, then pass concrete values into the prompt.\n\n### `Inbox` (Email Isolation)\n\nInstall: `npm install -D @stablyai/email`. Requires `STABLY_API_KEY` and `STABLY_PROJECT_ID` env vars (or pass to `Inbox.build()`).\n\n```ts\nconst inbox = await Inbox.build({ suffix: `test-${Date.now()}` });\n// inbox.address → \"my-org+test-1706621234567@mail.stably.ai\"\n\nawait page.getByLabel(\"Email\").describe(\"Email input\").fill(inbox.address);\n\nconst email = await inbox.waitForEmail({ subject: \"verification\", timeoutMs: 60_000 });\nconst { data: otp } = await inbox.extractFromEmail({\n  id: email.id,\n  prompt: \"Extract the 6-digit OTP code\",\n});\n\nawait inbox.deleteAllEmails();\n```\n\n#### Inbox.build Options\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `suffix` | string | Suffix for test isolation (e.g., `\"test-123\"` → `\"org+test-123@mail.stably.ai\"`) |\n| `apiKey` | string | Defaults to `STABLY_API_KEY` env var |\n| `projectId` | string | Defaults to `STABLY_PROJECT_ID` env var |\n\nAlways use a unique `suffix` per test for parallel isolation. The inbox automatically filters out emails received before it was created.\n\n#### waitForEmail\n\n```ts\nconst email = await inbox.waitForEmail({\n  from: \"noreply@example.com\",    // filter by sender\n  subject: \"verification\",         // contains match by default\n  subjectMatch: \"exact\",           // or \"contains\" (default)\n  timeoutMs: 60_000,               // default: 120000 (2 min)\n  pollIntervalMs: 5000,            // default: 3000 (3 sec)\n});\n```\n\nThrows `EmailTimeoutError` if no match arrives within the timeout.\n\n#### extractFromEmail\n\nReturns `{ data, reason }`. Throws `EmailExtractionError` on failure.\n\n```ts\n// String extraction\nconst { data: otp } = await inbox.extractFromEmail({\n  id: email.id,\n  prompt: \"Extract the 6-digit OTP code\",\n});\n\n// Structured extraction with Zod schema\nimport { z } from \"zod\";\nconst { data } = await inbox.extractFromEmail({\n  id: email.id,\n  prompt: \"Extract the verification URL and expiration time\",\n  schema: z.object({ url: z.string().url(), expiresIn: z.string() }),\n});\n```\n\n#### Inbox Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `address` | string | Full email address (with suffix if provided) |\n| `suffix` | string \\| undefined | The suffix passed to `Inbox.build()` |\n| `createdAt` | Date | Inbox creation time; emails before this are auto-filtered |\n\n#### listEmails\n\n```ts\nconst { emails, nextCursor } = await inbox.listEmails(options?);\n```\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `from` | string | — | Filter by sender address |\n| `subject` | string | — | Filter by subject |\n| `subjectMatch` | `'contains'` \\| `'exact'` | `'contains'` | Subject matching mode |\n| `limit` | number | 20 | Max results (max: 100) |\n| `cursor` | string | — | Pagination cursor from previous `nextCursor` |\n| `since` | Date | — | Override the default creation-time filter |\n| `includeOlder` | boolean | false | Include emails received before inbox creation |\n\n#### Other Methods\n\n```ts\nconst email = await inbox.getEmail(id);                          // get by ID\nawait inbox.deleteEmail(email.id);                               // delete single\nawait inbox.deleteAllEmails();                                   // delete all (this inbox only)\n```\n\n#### Email Object Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `id` | string | Unique identifier |\n| `mailbox` | string | Container (e.g., `\"INBOX\"`) |\n| `from` | `{ address: string, name?: string }` | Sender |\n| `to` | `{ address: string, name?: string }[]` | Recipients |\n| `subject` | string | Subject line |\n| `receivedAt` | Date | Arrival timestamp |\n| `text` | string? | Plain text body |\n| `html` | string[]? | HTML body parts |\n\n#### Playwright Fixture Pattern\n\n```ts\nimport { test as base } from \"@stablyai/playwright-test\";\nimport { Inbox } from \"@stablyai/email\";\n\nconst test = base.extend<{ inbox: Inbox }>({\n  inbox: async ({}, use, testInfo) => {\n    const inbox = await Inbox.build({ suffix: `test-${testInfo.testId}` });\n    await use(inbox);\n    await inbox.deleteAllEmails();\n  },\n});\n\ntest(\"signup flow\", async ({ page, inbox }) => {\n  await page.fill(\"#email\", inbox.address);\n  await page.click(\"#signup\");\n  const email = await inbox.waitForEmail({ subject: \"Welcome\" });\n  // ...\n});\n```\n\n### Finding Your Organization's Email Address\n\nYour email address is visible in the Stably dashboard:\n- **Settings > Email Inbox**: Displays the full address with a copy button\n- **In code**: `inbox.address` after calling `Inbox.build()` returns your full address\n\nThe 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.\n\nDirect users to the dashboard Settings > Email Inbox to find their specific address.\n\n## Auth Flows (Google)\n\nUse the helper instead of custom popup scripting:\n\n```ts\nimport { authWithGoogle } from \"@stablyai/playwright-test/auth\";\n\nawait authWithGoogle({\n  context,\n  email: process.env.GOOGLE_AUTH_EMAIL!,\n  password: process.env.GOOGLE_AUTH_PASSWORD!,\n  otpSecret: process.env.GOOGLE_AUTH_OTP_SECRET!,\n});\n```\n\nRequired env vars:\n- `GOOGLE_AUTH_EMAIL`\n- `GOOGLE_AUTH_PASSWORD`\n- `GOOGLE_AUTH_OTP_SECRET`\n\nUse a dedicated test Google account only.\n\n## Troubleshooting (Short)\n\n- `aiAssert` is slow/flaky: scope to a locator, tighten prompt, avoid unnecessary `fullPage: true`.\n- `agent.act` fails: split into smaller tasks, pass explicit constraints, raise `maxCycles` only when needed.\n- Email timeout: verify subject/from filter and use unique inbox suffixes.\n\n## Full References\n\n- Stably docs: https://docs.stably.ai\n- SDK setup skill: `skills/stably-sdk-setup/SKILL.md`\n- Package docs: https://www.npmjs.com/package/@stablyai/playwright-test\n- Email package docs: https://www.npmjs.com/package/@stablyai/email\n- Local overview: `skills/stably-sdk-rules/README.md`","tags":["stably","sdk","rules","agent","skills","stablyai","agent-skills"],"capabilities":["skill","source-stablyai","skill-stably-sdk-rules","topic-agent-skills"],"categories":["agent-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/stablyai/agent-skills/stably-sdk-rules","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add stablyai/agent-skills","source_repo":"https://github.com/stablyai/agent-skills","install_from":"skills.sh"}},"qualityScore":"0.453","qualityRationale":"deterministic score 0.45 from registry signals: · indexed on github topic:agent-skills · 6 github stars · SKILL.md body (8,831 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T19:15:07.245Z","embedding":null,"createdAt":"2026-05-18T13:22:56.187Z","updatedAt":"2026-05-18T19:15:07.245Z","lastSeenAt":"2026-05-18T19:15:07.245Z","tsv":"'-123':495 '/package/@stablyai/email':1036 '/package/@stablyai/playwright-test':1030 '000':465,561 '1':52,377 '1.54.1':364 '10':149 '100':706 '11':159 '12':168 '120000':563 '2':61,564 '20':702 '3':70,570 '3000':569 '4':85 '5':96 '5000':567 '6':108,476,602 '60':464,560 '7':119 '8':126 '9':140 'account':976 'actions/assertions':58 'activ':357 'add':161,905 'address':641,645,687,771,777,859,862,875,889,925 'agent.act':42,72,142,256,386,389,993 'ai':5,25,384 'aiassert':39,88,244,285,290,304,980 'allowlist':902,912 'alway':160,516 'api':193,216,424,503 'apikey':498 'arriv':577,788 'assert':92,237,241,316 'async':820,838 'au':46 'auth':926,947,951,955,962,965,968 'authwithgoogl':939,943 'auto':668 'auto-filt':667 'automat':528 'avatar':307 'avoid':989 'await':287,298,326,352,369,378,388,439,449,459,469,480,541,595,617,675,737,743,748,825,830,833,841,845,850,942 'base':77,807 'base.extend':816 'bash':184 'becom':68 'bell':309 'block':183 'bodi':794,798 'boolean':724 'brittl':69,258 'button':373,381,879 'call':884 'canva':74 'canvas/coordinates':263 'card':297 'chart':294 'cheaper':60 'check':247 'choic':238,249 'clean':179 'click':385 'code':148,406,479,605,881 'comput':403 'concret':409 'const':324,343,350,366,437,457,466,539,592,615,672,735,814,823,848 'constraint':271,1001 'contain':133,550,557,694,696,767 'content':155,321 'context':139,944 'coordin':76 'coordinate-bas':75 'copi':878 'core':227 'cost':65 'count':368,375 'cover':27 'creat':536 'createdat':658 'creation':661,720,731 'creation-tim':719 'cross':274 'cross-step':273 'cursor':707,710 'custom':934 'd':187,420 'dashboard':868,917 'data':106,276,467,583,593,616 'date':659,715,787 'date.now':443 'dedic':973 'default':500,509,553,558,562,568,680,718 'delet':746,750 'describ':162,236,302,452 'descript':486,640,681,760 'determinist':57,253 'digit':477,603 'direct':913 'display':872 'doc':1020,1027,1033 'docs.stably.ai':1021 'drag/click':78 'dynam':90 'dynamic/visual-heavy':246 'e.g':493,768 'email':124,170,415,451,453,458,531,540,644,663,673,727,736,755,843,849,858,861,870,909,919,945,948,963,1007,1031 'email.id':472,598,620,745 'emailextractionerror':586 'emailtimeouterror':573 'env':430,505,514,959 'especi':262 'everi':231 'exact':555,695 'expect':87,207,288,299,374 'expir':627 'expiresin':634 'explicit':267,1000 'export':191,197 'extract':40,107,322,328,474,591,600,607,622 'extractfromemail':581 'fail':994 'failur':588 'fals':725 'faster':59 'featur':26 'fill':455 'filter':529,545,669,684,690,722,1011 'find':390,854,922 'first':242,334,392 'fixtur':801 'flow':84,125,837,927 'full':643,874,888,1017 'fullpag':151,312,991 'get':354,740 'getlocatorsbyai':41,361 'good':401 'googl':45,928,961,964,967,975 'hard':115 'header':301,303 'helper':931 'html':795,797 'icon':310 'id':200,203,331,429,471,513,597,619,739,742,761 'identifi':764 'implicit':137 'import':205,210,220,339,611,804,810,938 'inbox':43,121,211,414,438,527,636,660,730,753,769,811,817,818,819,824,832,840,871,920,1015 'inbox.address':444,456,844,882 'inbox.build':174,435,440,482,657,826,885 'inbox.deleteallemails':481,749,834 'inbox.deleteemail':744 'inbox.extractfromemail':470,596,618 'inbox.getemail':738 'inbox.listemails':676 'inbox.waitforemail':460,542,851 'includ':266,726 'includeold':723 'input':454 'instal':186,417,419 'instead':932 'intent':269 'interact':233,248 'isol':171,416,492,525 'keep':93,141 'key':194,196,217,226,425,504 'limit':700 'line':785 'listemail':670 'local':1037 'locat':118,164,229,232,367,382,986 'locator.describe':379 'locator.extract':99 'login':372,380 'loops/calculations/conditionals':146 'mail.stably.ai':896,906 'mailbox':765 'mark':396 'match':551,576,698 'matter':158 'max':703,705 'maxcycl':1003 'method':34,733 'metric':351 'min':565 'minim':36,282 'mode':699 'modifi':20 'multi':82 'multi-step':81 'must':129 'my-org':445 'name':773,779,895 'nav':306 'need':102,317,900,1006 'never':134 'nextcursor':674,713 'noreply@example.com':544 'npm':185,418 'number':701 'object':756 'off-screen':318 'option':214,483,484,677,678 'order':330,394 'orderid':325 'org':447,496,894 'org-nam':893 'organ':856 'otp':468,478,594,604,956,969 'otp/magic-link/verification':123 'otpsecret':953 'outsid':156 'overrid':716 'overview':1038 'packag':1026,1032 'page':289,400,839 'page.click':846 'page.extract':98,327,353 'page.fill':842 'page.getbylabel':450 'page.getlocatorsbyai':110,370 'page.locator':300 'pagin':709 'parallel':524 'part':799 'pass':272,408,433,655,999 'password':949,952,966 'pattern':37,284,402,802,891 'pend':393 'per':176,521 'plain':792 'playwright':12,21,31,55,67,240,251,363,800 'playwright/test':188 'plus':35 'pollintervalm':566 'popup':935 'prefer':53 'previous':138,712 'priorit':62 'process.env.google':946,950,954 'programmat':218 'project':199,202,428,512 'projectid':507 'prompt':94,128,264,413,473,599,621,988 'properti':637,638,757,758 'provid':649,910 'qualiti':265 'quick':50 'rais':1002 'raw':54 'readabl':167 'reason':584 'receiv':532,728 'receivedat':786 'recipi':781 'refer':281,1018 'reli':135 'reliabl':63 'requir':362,422,958 'result':704 'return':582,886 'revenu':292,346,355 'row':335 'rule':4,6,49,51,228,230 'schema':337,344,359,360,610,629 'scope':983 'screen':320 'script':936 'sdk':3,13,48,1022 'sec':571 'secret':957,970 'selector':113 'self':132 'self-contain':131 'semant':112,260 'sender':547,686,775 'set':215,869,918 'setapikey':221,224 'setup':181,1023 'ship':399 'short':979 'show':291 'signup':836,847 'sinc':714 'singl':182,747 'skill':16,1024 'skill-stably-sdk-rules' 'skills/stably-sdk-rules/readme.md':1039 'skills/stably-sdk-setup/skill.md':1025 'slow/flaky':982 'small':144 'smaller':997 'source-stablyai' 'specif':95,924 'split':995 'spotlight':296 'stabli':2,11,24,33,47,192,198,423,427,502,511,867,1019 'stably-sdk-rul':1 'stablyai/email':190,213,421,813 'stablyai/playwright-test':189,209,223,809 'stablyai/playwright-test/auth':941 'standard':117 'step':83,254,275 'string':488,499,508,590,642,651,683,689,708,762,766,772,774,778,780,783,791,796 'structur':606 'subject':461,548,688,692,697,782,784,852 'subject/from':1010 'subjectmatch':554,693 'suffix':175,441,487,489,520,647,650,654,827,1016 'target':268 'task':143,261,998 'test':9,22,177,206,442,491,494,522,805,815,828,835,974 'test-123@mail.stably.ai':497 'test-1706621234567@mail.stably.ai':448 'testinfo':822 'testinfo.testid':829 'text':790,793 'throw':572,585 'tighten':987 'time':628,662,721 'timeout':580,1008 'timeoutm':463,559 'timestamp':789 'tobe':376 'topic-agent-skills' 'trace':166 'trend':293 'troubleshoot':978 'true':152,313,992 'ts':204,219,286,323,338,365,387,436,538,589,671,734,803,937 'type':485,639,679,759 'undefin':652 'uniqu':173,519,763,1014 'unnecessari':990 'unstabl':80 'url':625,631,633 'usag':283 'use':14,30,71,86,97,109,120,150,172,235,239,243,250,255,311,517,821,831,929,971,1013 'user':348,358,899,914 'vagu':280 'valu':404,410 'var':431,506,515,960 'variabl':278 'verif':462,549,624 'verifi':1009 'viewport':157 'visibl':864 'visual':91,104 'visual-to-data':103 'vs':32 'waitforemail':537 'welcom':853 'within':578 'write':8,18 'www.npmjs.com':1029,1035 'www.npmjs.com/package/@stablyai/email':1034 'www.npmjs.com/package/@stablyai/playwright-test':1028 'z':340,612 'z.number':349 'z.object':345,630 'z.string':347,632,635 'zod':342,609,614","prices":[{"id":"ebfed34e-1c73-4edf-b680-f0bbe3c1095e","listingId":"caae3119-4edd-49e4-952a-3180e9e73146","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"stablyai","category":"agent-skills","install_from":"skills.sh"},"createdAt":"2026-05-18T13:22:56.187Z"}],"sources":[{"listingId":"caae3119-4edd-49e4-952a-3180e9e73146","source":"github","sourceId":"stablyai/agent-skills/stably-sdk-rules","sourceUrl":"https://github.com/stablyai/agent-skills/tree/main/skills/stably-sdk-rules","isPrimary":false,"firstSeenAt":"2026-05-18T13:22:56.187Z","lastSeenAt":"2026-05-18T19:15:07.245Z"}],"details":{"listingId":"caae3119-4edd-49e4-952a-3180e9e73146","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"stablyai","slug":"stably-sdk-rules","github":{"repo":"stablyai/agent-skills","stars":6,"topics":["agent-skills"],"license":null,"html_url":"https://github.com/stablyai/agent-skills","pushed_at":"2026-04-15T08:16:48Z","description":"AI agent skills for the Stably Playwright SDK.","skill_md_sha":"82bf042852ef9505d147762f02f9d4ca4608ccd4","skill_md_path":"skills/stably-sdk-rules/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/stablyai/agent-skills/tree/main/skills/stably-sdk-rules"},"layout":"multi","source":"github","category":"agent-skills","frontmatter":{"name":"stably-sdk-rules","license":"MIT","description":"AI rules for writing tests with Stably Playwright SDK.\nUse this skill when writing or modifying Playwright tests with Stably AI\nfeatures. Covers when to use Playwright vs Stably methods, plus minimal\npatterns for aiAssert, extract, getLocatorsByAI, agent.act, Inbox, and\nGoogle auth."},"skills_sh_url":"https://skills.sh/stablyai/agent-skills/stably-sdk-rules"},"updatedAt":"2026-05-18T19:15:07.245Z"}}