$npx -y skills add arimanyus/hermes-merchant --skill greenhouse-job-applicationAutomate filling job applications on Greenhouse.io and similar React-based ATS platforms via browser automation. Covers React input handling, reCAPTCHA detection, resume upload limitations, and single-expression multi-field filling.
| 1 | # Greenhouse.io Job Application Automation |
| 2 | |
| 3 | Automate filling job applications on Greenhouse.io (and similar React-based ATS platforms) via browser automation. |
| 4 | |
| 5 | ## When to Use |
| 6 | - User asks to apply to a job on Greenhouse.io |
| 7 | - Filling out web forms with profile data |
| 8 | - Uploading resumes to job application systems |
| 9 | |
| 10 | ## Configuration |
| 11 | |
| 12 | Reads PII and the resume path from `profile.yaml` at the repo root (copy from `profile.yaml.example` on first run). Relevant keys: |
| 13 | |
| 14 | - `identity.first_name`, `last_name`, `email`, `phone`, `country` |
| 15 | - `resume.path` |
| 16 | - `work_authorization.us_authorized`, `work_authorization.needs_visa_sponsorship` |
| 17 | - `careers_emails` — fallback for when React dropdowns block submission |
| 18 | - `paths.applier_log` |
| 19 | |
| 20 | ## Form Filling (Single JavaScript Expression) |
| 21 | |
| 22 | React-controlled inputs ignore `element.value = x`. Use the native setter. Load values from `profile.yaml` and inject them into one `browser_console` call: |
| 23 | |
| 24 | ```python |
| 25 | import yaml |
| 26 | from pathlib import Path |
| 27 | |
| 28 | cfg = yaml.safe_load(Path("profile.yaml").read_text()) |
| 29 | fields = { |
| 30 | "first_name": cfg["identity"]["first_name"], |
| 31 | "last_name": cfg["identity"]["last_name"], |
| 32 | "email": cfg["identity"]["email"], |
| 33 | "phone": cfg["identity"]["phone"], |
| 34 | "country": cfg["identity"]["country"], |
| 35 | } |
| 36 | ``` |
| 37 | |
| 38 | Then execute the snippet below, injecting `fields` as `fieldMap`: |
| 39 | |
| 40 | ```javascript |
| 41 | (function() { |
| 42 | function setReactValue(element, value) { |
| 43 | var nativeInputValueSetter = Object.getOwnPropertyDescriptor( |
| 44 | window.HTMLInputElement.prototype, 'value' |
| 45 | ).set; |
| 46 | nativeInputValueSetter.call(element, value); |
| 47 | var event = new Event('input', { bubbles: true }); |
| 48 | element.dispatchEvent(event); |
| 49 | } |
| 50 | |
| 51 | var fieldMap = /* INJECT fields FROM profile.yaml */; |
| 52 | |
| 53 | for (var id in fieldMap) { |
| 54 | var el = document.getElementById(id); |
| 55 | if (el) setReactValue(el, fieldMap[id]); |
| 56 | } |
| 57 | |
| 58 | return { status: 'completed', fields: Object.keys(fieldMap) }; |
| 59 | })() |
| 60 | ``` |
| 61 | |
| 62 | **Important**: fill ALL fields in ONE `browser_console` call. Do NOT use individual `browser_type` calls — per-turn iteration limits will be hit. |
| 63 | |
| 64 | ## Verify Fields After Filling |
| 65 | |
| 66 | React may reset values. Always verify in a second call: |
| 67 | |
| 68 | ```javascript |
| 69 | (function() { |
| 70 | var checks = ['first_name', 'last_name', 'email', 'phone', 'country']; |
| 71 | var result = {}; |
| 72 | for (var i = 0; i < checks.length; i++) { |
| 73 | var el = document.getElementById(checks[i]); |
| 74 | result[checks[i]] = el ? el.value : 'NOT FOUND'; |
| 75 | } |
| 76 | return result; |
| 77 | })() |
| 78 | ``` |
| 79 | |
| 80 | ## Resume Upload |
| 81 | |
| 82 | **Limitation**: you cannot read local files from browser context. The `DataTransfer` API creates an empty `File` — upload will fail server-side validation. |
| 83 | |
| 84 | An approach that documents the attempt: |
| 85 | |
| 86 | ```javascript |
| 87 | (function() { |
| 88 | var file = new File([new ArrayBuffer(0)], 'resume.pdf', {type: 'application/pdf'}); |
| 89 | var dt = new DataTransfer(); |
| 90 | dt.items.add(file); |
| 91 | var resumeInput = document.getElementById('resume'); |
| 92 | if (resumeInput) { |
| 93 | resumeInput.files = dt.files; |
| 94 | return { resumeUpload: 'attempted', filesCount: resumeInput.files.length }; |
| 95 | } |
| 96 | return { resumeUpload: 'no_resume_field' }; |
| 97 | })() |
| 98 | ``` |
| 99 | |
| 100 | For real resume uploads, switch to CDP and use `DOM.setFileInputFiles` with the resume path from `cfg["resume"]["path"]` — see [`../browser-harness-ats-automation/SKILL.md`](../browser-harness-ats-automation/SKILL.md). |
| 101 | |
| 102 | ## reCAPTCHA Detection |
| 103 | |
| 104 | Check before attempting submit: |
| 105 | |
| 106 | ```javascript |
| 107 | (function() { |
| 108 | var recaptcha = document.querySelector('.g-recaptcha, [data-recaptcha], iframe[src*="recaptcha"]'); |
| 109 | return { hasRecaptcha: !!recaptcha }; |
| 110 | })() |
| 111 | ``` |
| 112 | |
| 113 | ## Submission |
| 114 | |
| 115 | If reCAPTCHA is present: |
| 116 | 1. Click submit via `browser_click`. |
| 117 | 2. Expect validation errors — this is normal. |
| 118 | 3. Log partial success and note that manual completion is needed. |
| 119 | |
| 120 | ## Logging |
| 121 | |
| 122 | Append to `paths.applier_log` from `profile.yaml`. Include: |
| 123 | |
| 124 | - Timestamp, job title, company, URL |
| 125 | - Fields filled (`✓`/`✗`) |
| 126 | - Resume upload result |
| 127 | - Submission result (success / partial / blocked) |
| 128 | - Next steps for manual completion |
| 129 | |
| 130 | ## React Select / Dropdown Components (critical — these cannot be automated) |
| 131 | |
| 132 | Greenhouse uses custom React dropdowns (`class="select__input"`, `role="combobox"`, `aria-expanded`, `role="listbox"` with `role="option"` children). These are **NOT** standard `<select>` elements — they are `div`/`input` hybrids that ignore: |
| 133 | |
| 134 | - Standard `click()` on the input field |
| 135 | - `type_text()` into the input |
| 136 | - The React-value-setter technique used for text fields |
| 137 | - Clicking `[role="option"]` elements directly (click registers but React state doesn't update) |
| 138 | |
| 139 | **Symptoms**: `element.value` stays `''`, `aria-expanded` may toggle but options don't filter, and submission fails validation. |
| 140 | |
| 141 | **Affected fields typically include**: |
| 142 | - |