I Built a LinkedIn Easy Apply Bot in Python Hereβs What I Learned About Browser Automation
The Problem
Applying for jobs online can become repetitive very quickly. Many application forms ask for the same basic information:
- First name
- Last name
- Phone number
- City
- CV upload
- Work authorization
- Notice period
- Years of experience
- Salary expectation
When someone is actively searching for jobs, they may fill the same information many times across different listings. The goal of this project was simple: Can I build a small automation assistant that reduces repetitive form filling while keeping the user in control?
What the Project Does
The project is a Python CLI tool that opens Chrome, logs into LinkedIn, searches for Easy Apply jobs, fills simple application forms, uploads a CV when required, and records successful applications. At a high level, the workflow is:
- Read user configuration from
config.json - Read LinkedIn login credentials from
.env - Open Chrome using Selenium
- Log in to LinkedIn
- Search for jobs using configured filters
- Find Easy Apply jobs
- Open each job application modal
- Fill known fields from saved answers
- Upload the configured CV
- Answer simple questions using saved answers and resume-derived information
- Submit the application only when the form is manageable
- Save the application record to avoid duplicates
The assistant also includes a --dry-run mode so the user can test login and search without submitting any applications.
Important Note About Responsible Use
This project is intended as a personal productivity and learning project. It is not designed for spam applying, bypassing platform protections, or violating website rules. Browser automation should be used carefully and responsibly. For that reason, I added safeguards such as:
- Dry-run mode
- Confirmation mode
- Rate limiting between actions
- Rate limiting between applications
- Duplicate tracking
- Manual CAPTCHA / 2FA handling
- Skipping complex or unknown forms
- Configuration validation before running
The goal is not to apply to hundreds of jobs blindly. The goal is to reduce repetitive work while keeping the process controlled and human-supervised.
Tech Stack
The project uses:
- Python
- Selenium
- Chrome WebDriver
- python-dotenv
- webdriver-manager
- pypdf
- JSON / CSV tracking
The main project files are:
main.py- CLI entry pointconfig.py- Loads and validates configurationlinkedin_automation.py- Selenium browser automationresume_profile.py- Extracts resume informationtracker.py- Tracks applied jobssession_store.py- Stores/reuses LinkedIn session cookieserrors.py- User-friendly error handling
Project Architecture
The project is split into small modules so that each file has a clear responsibility.
main.py
This is the entry point of the application. It handles:
- CLI arguments
- Config loading
- Validation
- Browser startup
- Login flow
- Job search navigation
- Application loop
- Run summary
Some useful commands are:
python main.py --validate-only
This checks the setup without opening the browser.
python main.py --dry-run
This logs in and searches jobs but does not submit applications.
python main.py --confirm --pause-on-challenge --max-applications 5
This runs the assistant with user confirmation, CAPTCHA/2FA support, and a maximum application limit.
Configuration Design
The project separates secrets from normal configuration. LinkedIn credentials are stored in .env:
LINKEDIN_EMAIL=your-email@example.com
LINKEDIN_PASSWORD=your-password
The job search settings and personal answers are stored in config.json:
{
"search": {
"keywords": "software engineer",
"location": "United Kingdom",
"work_type": "2",
"job_type": "F",
"date_posted": "r604800",
"experience_level": "3,4"
},
"max_applications": 5,
"resume_path": "C:/path/to/resume.pdf",
"tracking": {
"output_file": "applications.json",
"format": "json"
},
"saved_answers": {
"first_name": "Arul",
"last_name": "Cornelious",
"email": "your-email@example.com",
"phone": "your-phone-number",
"city": "St Albans",
"salary": "Negotiable",
"sponsorship": "Yes",
"start_date": "Immediately"
},
"custom_answers": {
"years of experience with angular": "3",
"are you willing to relocate": "Yes"
}
}
This design keeps sensitive credentials out of the main configuration file.
Building the LinkedIn Job Search URL
Instead of manually clicking filters, the assistant builds a LinkedIn job search URL using query parameters. For example, it can include:
- Keywords
- Location
- Easy Apply filter
- Work type
- Job type
- Date posted
- Experience level
- Few applicants filter
- LinkedIn geo ID
The Easy Apply filter is applied through the URL so the assistant focuses only on jobs that support LinkedIn's Easy Apply workflow. This makes the search flow simpler and more predictable.
Selenium Automation
The browser automation is handled with Selenium. The assistant opens Chrome, logs into LinkedIn, searches jobs, and interacts with the Easy Apply modal.
One challenge with browser automation is that websites often change their HTML structure. To make the project more stable, I used multiple CSS selectors and XPath fallbacks for important elements like:
- Job cards
- Easy Apply buttons
- Modal buttons
- Submit buttons
- Next buttons
- Review buttons
For example, the assistant does not rely on only one selector for the Easy Apply button. It checks multiple possible selectors and also uses text-based fallback logic. This makes the automation more resilient when LinkedIn changes small parts of the UI.
Login and Session Reuse
Logging in every time can trigger extra verification. To reduce that, the assistant stores session cookies after a successful login and reuses them in later runs. The login flow supports:
- Normal email/password login
- Saved session reuse
- Fresh login mode
- CAPTCHA / 2FA pause mode
If LinkedIn asks for verification, the assistant can pause and allow the user to complete the challenge manually in the browser. Example:
python main.py --pause-on-challenge
This keeps the process human-supervised instead of trying to bypass security checks.
Resume-Based Question Answering
One of the most interesting parts of the project is the resume-based question answering system. The assistant can read the configured CV and extract useful information such as:
- Skills
- Total years of experience
- Skill-specific experience
- Work authorization text
- Notice period
- Education level
- Phone number
It supports PDF extraction using pypdf. The answer priority is:
custom_answers- resume-derived profile
saved_answers
This means manually configured answers always win. For example, if the application asks: "How many years of experience do you have with Angular?" The assistant checks:
- Is there a matching custom answer?
- Is Angular found in the resume?
- Can it estimate experience from the resume?
- If not, should the question be skipped?
This prevents the assistant from guessing too aggressively.
Handling Unknown Questions
Not every application form is simple. Some forms include custom questions, long text answers, dropdowns, multi-step flows, or questions that require human judgement. The assistant is designed to skip forms it cannot confidently complete. If it finds a question it cannot answer, the user can add it later to custom_answers. Example:
{
"custom_answers": {
"do you require visa sponsorship": "Yes",
"what is your expected salary": "Negotiable",
"are you willing to work hybrid": "Yes"
}
}
This makes the system improve over time while still keeping the user in control.
Tracking Applications
The assistant records every successful application in a tracking file. Example JSON output:
[
{
"job_title": "Software Engineer",
"company_name": "Example Company",
"job_url": "https://www.linkedin.com/jobs/view/123456789/",
"date_applied": "2026-07-03 12:00:00",
"status": "applied"
}
]
This solves two problems:
- The user can review application history.
- The assistant can avoid applying to the same job twice.
The project supports both JSON and CSV tracking.
Rate Limiting
Rate limiting is important in browser automation. The project includes two types of delay:
delay_between_actions_secdelay_between_applications_sec
The first delay controls normal browser actions like clicks and page loads. The second delay controls how long the assistant waits after submitting an application. This helps keep the automation slower, safer, and more human-like.
CLI Flags
I added several CLI flags to make the tool safer and easier to test.
--dry-run- Logs in and searches jobs but does not apply.--confirm- Shows a confirmation prompt before live application submission.--max-applications 5- Limits how many applications can be submitted in one run.--pause-on-challenge- Pauses when CAPTCHA or 2FA appears.--fresh-login- Ignores saved session cookies and logs in again.--validate-only- Checks.envandconfig.jsonwithout opening the browser.
These flags are useful because browser automation should be tested carefully before any real action is performed.
Challenges I Faced
LinkedIn UI changes - LinkedIn's DOM can change, which means selectors can break. To handle this, I used multiple selector strategies and fallbacks.
Easy Apply forms are not always the same - Some applications are one step. Some are multiple steps. Some ask custom questions. Some require dropdowns, radio buttons, or file uploads. The assistant handles simple and predictable forms, but skips complex ones.
Avoiding duplicate applications - Raw LinkedIn job URLs can include tracking parameters, so the same job can appear with different URLs. To fix this, I normalized job URLs into a cleaner format before tracking them.
Not over-automating - The project needed a balance between automation and responsibility. That is why I added dry-run mode, confirmation mode, manual challenge handling, delays, and skipping logic.
What I Learned
This project helped me understand several practical engineering concepts:
- Browser automation with Selenium
- CLI design in Python
- Configuration management
- Environment variable handling
- Resume parsing
- Form-filling logic
- URL normalization
- Error handling
- Rate limiting
- Session cookie reuse
- Designing safer automation workflows
It also reminded me that automation is not just about making things faster. Good automation should also be controlled, explainable, and respectful of user intent.
Future Improvements
Some improvements I would like to add next:
- Better dashboard for application history
- Export reports by date, company, and role
- Better support for dropdowns and radio buttons
- More detailed skipped-job reasons
- Safer preview mode before submitting each application
- Local encrypted credential storage
- Unit tests for resume parsing and answer matching
- GitHub Actions workflow for linting and tests
- Optional manual review step before final submit
Final Thoughts
This project started as a simple idea: reduce repetitive job application steps. But it became a useful engineering exercise in browser automation, form intelligence, configuration design, safety controls, and responsible automation.
The biggest lesson I learned is that automation should not remove human judgement. It should support it. For job applications, that means helping with repetitive form filling while still allowing the applicant to choose the right roles, review their details, and stay in control.
GitHub repository: https://github.com/Arul1998/linkedin-easy-apply
Thanks for reading. I'm open to feedback, suggestions, and ideas for making this project safer and more useful.
Comments
No comments yet. Start the discussion.