# ATS Form Automation Details ## SmartRecruiters Form Automation - SmartRecruiters uses 36+ shadow roots with SPL-* web components (Lit elements) - Standard querySelector CANNOT find elements inside shadow DOM — use recursive deepQuery: ```js function dqAll(r,sel){var results=[];r.querySelectorAll(sel).forEach(function(e){results.push(e);}); r.querySelectorAll('*').forEach(function(e){if(e.shadowRoot){dqAll(e.shadowRoot,sel).forEach(function(f){results.push(f);});}});return results;} ``` - **Personal info inputs**: `#first-name-input`, `#last-name-input`, `#email-input`, `#confirm-email-input`, phone via `spl-form-element`, `#linkedin-input` - **Setting input values**: Must use React native setter + dispatch events: ```js Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(el,val); el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); ``` - **Textarea** (cover letter): Same pattern but with `HTMLTextAreaElement.prototype` - **File upload**: Browser blocks JS `.click()` on file inputs. Workaround: local HTTP server + fetch + DataTransfer: ```js var resp=await fetch('http://localhost:PORT/file.pdf'); var file=new File([await resp.blob()],'file.pdf',{type:'application/pdf'}); var dt=new DataTransfer(); dt.items.add(file); fileInput.files=dt.files; fileInput.dispatchEvent(new Event('change',{bubbles:true})); ``` - **SPL-AUTOCOMPLETE dropdowns** (CRITICAL — most complex part): - JS `.click()` on `spl-select-option` does NOT update Lit component internal state - Physical clicks via cliclick are unreliable (coordinate drift between fields) - **WORKING APPROACH**: JS focus+clear via React setter, then System Events keystroke for typing, then keyCode Down+Enter: 1. `inp.focus()` + native setter to clear value + dispatch input/change events 2. `se.keystroke('unique filter text')` — type UNIQUE prefix to filter to exactly 1 option 3. Wait 1.5s for dropdown to filter 4. `se.keyCode(125)` (down) + `se.keyCode(36)` (enter) to select - MUST type enough text to uniquely match ONE option (e.g., "f. No, I am not a current" not just "No") - **SPL-RADIO-GROUP**: Find SPL-RADIO children, click the one with matching label - **SPL-BUTTON**: Find via `dqAll(document,'spl-button')`, get inner `button` via shadowRoot for reliable click - **Navigation**: "Next" button is typically `spl-button[type=primary]` index 2; "Submit" is index 1 on questions page - **Bot detection**: Chromium/Playwright gets blocked ("access temporarily restricted"). Must use real Chrome + macOS Automator (AppleScript/JXA) - **Playwright MCP CDP fails** on SmartRecruiters due to strict CSP blocking chrome-extension:// URLs - **cliclick pitfalls**: NEVER use `kd:cmd t:a ku:cmd` to select all — can trigger fullscreen or other shortcuts. Use triple-click (`tc:`) or JS clearing instead - **Chrome window offset**: bounds from AppleScript give left,top,right,bottom. Content area = left+viewportX, top+toolbar(~80px)+viewportY ## General Automation Strategy (Platform Decision Tree) - **Standard HTML forms** (job-room.ch, most ATS): Try Playwright MCP first → macOS Automator JXA fallback - **Ashby HQ** (jobs.ashbyhq.com): Playwright MCP works well — standard React app, no shadow DOM. Yes/No buttons need scrollIntoView before click - **Shadow DOM / Lit / Web Components** (SmartRecruiters): macOS Automator JXA only — Playwright MCP can't pierce shadow DOM reliably and gets blocked by CSP - **Workday**: macOS Automator JXA (Playwright blocked by extension CSP) - **Personio**: Playwright MCP works but datepicker needs browser_click + browser_type(slowly=true) - **Sword Services / Teamtailor** (sword-services.ch): Playwright MCP works with 1PW-safe pattern. MUST disable 1PW first (see global CLAUDE.md). Use browser_run_code with native setters for name/email, pressSequentially for phone, label clicks for radios, evaluate(el=>el.click()) for checkboxes, especially privacy checkbox. hCaptcha requires manual solve. Separate CV and cover letter upload fields. Form URL pattern: /l/en/o/{slug}/c/new - **Chromium has no persistent sessions** — when using `--browser chromium` (no user-data-dir), sites requiring login (job-room.ch, etc.) need manual login each time. Navigate to the login page, tell user to log in, wait for confirmation before proceeding. (1Password API integration TBD) - **macOS Automator JXA pattern**: `Application('Google Chrome').windows[0].activeTab.execute({javascript: code})` for DOM, `Application('System Events').processes['Google Chrome']` for keyboard - **System Events key codes**: 125=down, 126=up, 36=return/enter, 53=escape, 48=tab, 49=space, 51=delete - **cliclick** (`/opt/homebrew/bin/cliclick`): Use for physical mouse clicks when needed. Key names: `esc`, `arrow-down`, `return`, `delete`, `tab`, `space`. NEVER use `kd:cmd` sequences (trigger OS shortcuts). Prefer JS focus + System Events over cliclick for typing into fields. - **Local HTTP server for file uploads**: `python3 -m http.server PORT` in artifacts dir, then fetch+File+DataTransfer in JS to bypass file input security ## Ashby HQ Form Automation (jobs.ashbyhq.com) - Used by: DualEntry (and likely other YC/VC-backed startups) - Standard HTML form — no shadow DOM, no web components, straightforward React app - **Yes/No toggle buttons**: CSS module classes, NOT aria-pressed. Selection state = `_active_y2cw4_58` class + dark bg `rgb(15,55,62)` - Playwright MCP `.click()` on Yes buttons does NOT stick if button is not scrolled into view - **WORKING APPROACH**: Use `browser_run_code` with `scrollIntoViewIfNeeded()` + `click({force:true})` + delay per button: ```js for (let i = 0; i < yesButtons.length; i++) { await yesButtons[i].scrollIntoViewIfNeeded(); await page.waitForTimeout(300); await yesButtons[i].click({ force: true }); await page.waitForTimeout(500); } ``` - The accessibility snapshot does NOT reflect [active] state on Yes buttons — verify via JS `classList.contains('_active_y2cw4_58')` - **File upload**: Works via standard Playwright `browser_file_upload` after clicking "Upload File" button (triggers file chooser) - **Location combobox**: Type slowly, wait for dropdown, click matching option — standard combobox behavior - **Text fields**: Standard `.fill()` works fine for all textbox fields ## Sword Services Form Automation (sword-services.ch) - **Platform**: Teamtailor ATS — standard HTML forms, Playwright MCP works - **URL pattern**: `https://sword-services.ch/l/en/o/{job-slug}/c/new` - **Recurring provider**: Sword Group posts frequently — automate fully - **NEVER use `browser_fill_form` with multiple fields** — always fails on second field (ref e89 bug). Use individual `browser_type`/`browser_click` calls - **Phone number widget** (TRICKY): - Country code selector is a custom listbox, NOT a native select - `fill()` on the phone field resets the country code on blur - **WORKING APPROACH**: Use `browser_run_code` with `pressSequentially('+41764502698', {delay: 80})` after triple-click to clear. This auto-formats to "+41 76 450 26 98" and correctly sets country to "Suisse" - Do NOT try to set country separately then type digits — it reverts on other interactions - **Radio buttons**: Labels intercept pointer events on the ``. Click the label generic ref (e.g., e157 for "Suisse"), NOT the radio ref (e.g., e156) - **Checkboxes**: Click via `getByText('label text').click()` or the label generic ref - **Privacy checkbox** (CRITICAL): The label contains a `` link to the privacy policy. Clicking the label navigates away from the form! **MUST use `browser_run_code` with `el.evaluate(el => el.click())` on the checkbox input directly**, or use `getByText()` on the non-link part - **hCaptcha**: Appears after clicking "Envoyer" (submit). Image-based CAPTCHA — user must solve manually. Sometimes has "Passer" (Skip) button - **File uploads**: Standard — click upload button (triggers file chooser), then `browser_file_upload` with file path. Separate CV and CL upload fields - **Form fields (Matthias standard values)**: - Nom complet: Matthias Nott - Email: mnott@mnott.ch - Phone: +41764502698 (typed via pressSequentially) - Nationalité: Allemande - Résidence: Suisse (radio) - Permis: Citoyen.ne Suisse (radio) - Disponibilité: Immédiate - Lieu de résidence: Déja installé en Suisse (checkbox) - Mobilité: Genève + Lausanne (checkboxes, adjust per job location) - Privacy: Accept (checkbox, click via JS) - **Navigation survives back**: Form state partially persists on browser back (text fields, radios survive; phone digits may reset; checkbox state may reset) - **Recommended fill order**: (1) text fields, (2) phone via run_code, (3) radios via label click, (4) checkboxes + privacy via run_code batch, (5) file uploads, (6) pause for user before Envoyer - **PDF filenames**: Use descriptive names like "CV_Matthias_Nott_Chef_Projet_IT.pdf" not "CV_Geneva.pdf" - **Submit**: Single "Submit Application" button, success message: "Your application to build the future of ERP was successfully submitted" - **LinkedIn "Apply on company website" flow**: Button opens external URL in new tab that Playwright MCP cannot see. Use AppleScript to list all Chrome tabs and find the Ashby URL ## Playwright MCP Chrome-Extension Crash Pattern - **Root cause discovered**: Multiple Playwright MCP extension tabs accumulate in Chrome over time (visible via AppleScript tab listing) - The error `Protocol error (Target.setAutoAttach): Cannot access a chrome-extension:// URL of different extension` occurs when Playwright MCP tries to attach to a tab showing another extension's URL - **Site-specific**: Crashes happen more on sites that trigger extension interactions (job-room.ch with eIAM SAML auth, SmartRecruiters with CSP). Simple sites like Ashby work fine with Playwright - **Recovery sequence**: `pkill -f "@playwright/mcp"` → if still failing, `browser_close` → if still failing, quit Chrome entirely (`osascript -e 'tell application "Google Chrome" to quit'`) and let Playwright relaunch. Note: quitting Chrome loses session cookies and may require re-login - **Fallback**: When Playwright is unstable on a site, use JXA immediately instead of retrying ## Sword Services (Job Alerts) - Alert emails come from `noreply@tellentalerts.com` with subject "New opportunity at Sword Services for you" - Email body contains job title + location but NO clickable link - Careers page: `https://sword-services.ch/l/en/offres` (lists all open roles) - Individual JD URL pattern: `https://sword-services.ch/l/en/o/{slug}` — slug is title lowercased, spaces→hyphens (e.g., `mdm-technical-lead-architect-reltio`) - Recruitee-based ATS (redirects from swordservices.recruitee.com → sword-services.ch) - Offices: Geneva, Nyon, Lausanne, Fribourg, Sion ## Workday Form Automation - Chrome "Allow JavaScript from Apple Events" must be enabled manually (View > Developer menu) — automation toggle doesn't stick due to system confirmation dialog - Playwright MCP Bridge has persistent "Cannot access chrome-extension:// URL" issue — use AppleScript JS execution instead - Workday multiselect dropdowns resist synthetic JS clicks — physical keyboard/mouse via System Events may be needed for dropdowns - Text input fields work fine with native value setter + input/change events using element IDs (not data-automation-id) - Input field IDs follow pattern: `sectionName--fieldName` (e.g., `name--legalName--lastName`, `address--postalCode`)