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:
inp.focus() + native setter to clear value + dispatch input/change events
se.keystroke('unique filter text') — type UNIQUE prefix to filter to exactly 1 option
- Wait 1.5s for dropdown to filter
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 browserclick + browsertype(slowly=true)
- Sword Services / Teamtailor (sword-services.ch): Playwright MCP works with 1PW-safe pattern. MUST disable 1PW first (see global CLAUDE.md). Use browserruncode 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
<input>. 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
<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 runcode, (3) radios via label click, (4) checkboxes + privacy via runcode batch, (5) file uploads, (6) pause for user before Envoyer
- PDF filenames: Use descriptive names like "CVMatthiasNottChefProjetIT.pdf" not "CVGeneva.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)