LinkedIn Easy Apply Automation#
This skill helps an AI coding/operations agent build and run a repeatable LinkedIn Easy Apply workflow.
It is intentionally public and credential-free. It contains no usernames, passwords, cookies, private profile paths, or user-specific secrets.
What it does#
- Searches LinkedIn Jobs with Easy Apply enabled.
- Supports remote-only and location-constrained searches.
- Focuses on configurable role keywords such as AI, LLM, Claude, OpenAI, Codex, full-stack, frontend, backend, web, software, JavaScript, TypeScript, Node, React, and Svelte.
- Opens the LinkedIn Easy Apply flow reliably with a direct apply URL when possible.
- Uploads a verified resume PDF.
- Answers only verified applicant facts.
- Skips unknown required questions, compensation questions, custom essays, or subjective fields.
- Keeps state and JSONL logs so daily reruns avoid duplicates.
Safety rules#
- Do not store LinkedIn credentials in scripts, skills, memory, logs, or reports.
- Prefer a persistent browser profile where the operator logs in manually once.
- Stop for CAPTCHA, MFA, suspicious-login checks, identity verification, or account-security prompts.
- Do not fabricate applicant facts.
- Do not answer custom freeform questions or compensation expectations without operator-provided answers.
- Re-check full job descriptions for hybrid/location constraints before submitting remote-only applications.
Environment variables#
bash
RESUME_PDF=/absolute/path/to/resume.pdf
CHROME_PROFILE=$HOME/.cache/linkedin-chrome
STATE_DIR=/tmp/linkedin-easyapply-daily
MAX_SCAN=80
MAX_APPLY=10
DRY_RUN=1
SEARCHES='Claude|OpenAI|Codex|LLM engineer|AI engineer|full stack engineer|software engineer'
LOCATION='United States'
REMOTE_ONLY=1
Puppeteer launch pattern#
js
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
headless: false,
executablePath: process.env.CHROME_BIN || '/snap/bin/chromium',
userDataDir: process.env.CHROME_PROFILE || `${process.env.HOME}/.cache/linkedin-chrome`,
defaultViewport: null,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--start-maximized']
});
const page = await browser.newPage();
page.setDefaultTimeout(30000);
page.on('dialog', async d => {
try {
if (d.type() === 'beforeunload') await d.accept();
else await d.dismiss();
} catch {}
});
Search workflow#
Build LinkedIn Jobs URLs using explicit filters:
f_AL=true for Easy Apply.
- Remote filter when remote-only is required.
- Location such as
United States when requested.
- Configurable keywords matching the resume and target market.
Keep candidates only when the card/page supports the requested constraints and the title/description matches the target role family.
Direct Easy Apply URL#
When a LinkedIn job ID is known, try the direct flow first:
js
const applyUrl = `https://www.linkedin.com/jobs/view/${jobId}/apply/?openSDUIApplyFlow=true`;
await page.goto(applyUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
If the modal does not open, fall back to clicking visible Easy Apply controls across buttons and links.
Conservative answer rules#
Answer only facts that are verified by the resume, profile, or explicit operator instruction:
- Work authorization / eligible to work: yes only if verified.
- Visa sponsorship required: no only if verified.
- Remote willingness: yes only for remote-only searches.
- Years of experience: use resume-backed values or a user-approved mapping.
- Email/phone/location: use existing profile/resume facts.
- Middle name: blank if none provided.
Skip:
- Salary/compensation expectations.
- Cover-letter text prompts.
- Custom essays.
- Relocation/hybrid/travel if not approved.
- Unclear required inputs, radios, or selects.
Multi-step apply loop#
- Open the apply flow.
- Detect already-submitted applications.
- Upload/select the verified resume PDF if needed.
- Inspect each step's visible inputs/selects/radios.
- Fill only safe verified answers.
- Click
Next, Review, then Submit application only when no unknown required fields remain.
- Log submitted/skipped state.
Suggested state#
json
{
"seen": {},
"applied": {},
"skipped": {},
"alreadySubmitted": {}
}
Write state and logs to:
text
/tmp/linkedin-easyapply-daily/state.json
/tmp/linkedin-easyapply-daily/results.jsonl
Reporting#
Report concise results:
text
Submitted:
- Title — Company — URL
Skipped:
- Title — Company — URL — reason
State: /tmp/linkedin-easyapply-daily/state.json
Log: /tmp/linkedin-easyapply-daily/results.jsonl
Pitfalls#
- Built-in browser tools often time out on LinkedIn. Puppeteer with Chromium and Xvfb is usually more reliable.
- LinkedIn search cards can say Remote while the description includes hybrid/local constraints. Inspect the full page before applying.
- Broad keyword matching can create bad answers. Avoid treating generic words like
United States or comfortable as approval.
- Direct DOM value assignment may not update React forms. Prefer click/type and dispatch input/change events.
- If a page detaches or a modal closes unexpectedly, create a new page and continue from the state file.