I Built a LinkedIn Easy Apply Bot in Python Here’s What I Learned About Browser Automation
DEV Community

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
  • Email
  • 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 point
  • config.py - Loads and validates configuration
  • linkedin_automation.py - Selenium browser automation
  • resume_profile.py - Extracts resume information
  • tracker.py - Tracks applied jobs
  • session_store.py - Stores/reuses LinkedIn session cookies
  • errors.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
  • Email
  • Phone number

It supports PDF extraction using pypdf. The answer priority is:

  1. custom_answers
  2. resume-derived profile
  3. 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_sec
  • delay_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 .env and config.json without opening the browser.

These flags are useful because browser automation should be tested carefully before any real action is performed.

Challenges I Faced

  1. LinkedIn UI changes - LinkedIn's DOM can change, which means selectors can break. To handle this, I used multiple selector strategies and fallbacks.

  2. 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.

  3. 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.

  4. 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.