Samuel Fajreldines

I am a specialist in the entire JavaScript and TypeScript ecosystem.

I am expert in AI and in creating AI integrated solutions.

I am expert in DevOps and Serverless Architecture

I am expert in PHP and its frameworks.

+55 (51) 99226-5039 samuelfajreldines@gmail.com

Building an AI-Assisted LinkedIn Job Application Bot with Puppeteer, Bun & ChatGPT

Introduction

One of the things that made me fall in love with software engineering is the ability to build robots that can take care of tedious tasks for me.

Job hunting and applying to positions is definitely one of those tasks. It’s repetitive, time-consuming, and often frustrating. But what if we could automate it? What if we could help developers find the right roles faster—and help companies connect with the right talent more easily?

That’s exactly why I built this robot.

Motivation

Watching dozens of Easy Apply pop-ups each day is mind-numbing. I wanted a script that could:

Find roles that match my specific filters
Decide intelligently using AI if the job description really fits my background
Auto-fill forms by clicking through each step and answering questions with personalised, human-sounding responses

So I created: linkedin-jobs-applier — an easy way to find and apply to jobs that truly fit you.


Tech Stack

Layer Choice Why
Runtime Bun Fast start-up, native TypeScript support, zero-config bun install
Browser automation Puppeteer Mature Chrome API, excellent debugging with headless: false
Language TypeScript Strong typing for DOM handles & strict prompt validation
AI layer OpenAI ChatGPT Reliable completion API with multiple model options
Prompt storage about-me.txt Keeps personal data & CV separate from source code

Project Structure

src/
├── index.ts               # Bootstrap & global configuration
├── functions.ts           # Utility helpers (sleep, getText, getTextFromElement)
├── helpers/
│   └── ChatGptHelper.ts   # OpenAI API wrapper
└── services/
    ├── PuppeteerService.ts # Browser/page lifecycle management
    ├── LinkedInService.ts  # Search, pagination & login logic
    ├── JobCardService.ts   # Individual job processing & AI fit check
    └── ApplyService.ts     # Form navigation & automated filling
about-me.txt               # Your profile, resume & preferences

End-to-End Flow

1. Bootstrap Phase

index.ts loads your job search URL, reads your profile from about-me.txt, and launches the browser.

2. Authentication

LinkedInService.login() opens LinkedIn's sign-in page and waits for manual login completion (detected by avatar appearance).

3. Job Discovery

searchJobs() navigates to your search URL, scrolls through job cards, and implements recursive pagination with &start= offsets (25 jobs per page).

4. AI-Powered Filtering

For each job card, JobCardService: • Extracts job title & full description
• Asks ChatGPT: "Based on my profile, is this job a good fit? (YES/NO)"
• Only proceeds with applications for "YES" responses

5. Smart Form Filling

When a job passes the AI filter, ApplyService: • Opens the Easy Apply modal
• Iterates through each form step
• For every question, scrapes the label text
• Sends contextual prompts to ChatGPT including both profile and job description
• Maps AI responses to appropriate input types (text, select, radio, file upload, etc.)

6. Graceful Completion

After filling all fields, the bot either submits the application or gracefully discards it if errors occur. The process continues until LinkedIn's daily limit is reached, then sleeps for one hour.


Deep Dive by Component

1. index.ts – Configuration Hub

const puppeteer = require("puppeteer");
import { PuppeteerService } from "./services/PuppeteerService";
import { LinkedInService } from "./services/LinkedInService";
import fs from 'fs';

export const DEFINES = {
    JOB_LINK: `https://www.linkedin.com/jobs/search/?currentJobId=4234489284&distance=25&f_AL=true&f_WT=2&geoId=103644278&keywords=senior%20software%20engineer&origin=JOBS_HOME_SEARCH_CARDS`,
    ABOUT_ME: fs.readFileSync(__dirname + '/../about-me.txt', 'utf8')
}

const main = async () => {
    const linkedInService = new LinkedInService(await PuppeteerService.init());
    await linkedInService.login();
    await linkedInService.searchJobs();
}

main();

Why Bun? Because bun run executes TypeScript directly—no ts-node, no Babel, no configuration headaches.

2. PuppeteerService.ts – Browser Management

import puppeteer, { Browser, Page } from "puppeteer";

export class PuppeteerService {
    static async init(){
        const browser = await puppeteer.launch({
            headless: false,
            defaultViewport: null,
            args: ["--start-maximized"],
            userDataDir: "./.puppeteer-data",
        });

        const page = await browser.newPage();
        return new PuppeteerService(browser, page);
    }

    constructor(public browser: Browser, public page: Page) {}

    async goto(url: string){
        await this.page.goto(url);
    }
}

Key Feature: Running with headless: false makes debugging CSS selectors trivial—you can see exactly what the bot is doing.

3. LinkedInService.ts – Intelligent Navigation

async login(){
    await this.puppeteerService.goto("https://www.linkedin.com/login");

    let loggedIn = false;
    while (!loggedIn) {
        try{
            await this.puppeteerService.page.waitForSelector(".global-nav__me-photo", { timeout: 1000 });
            loggedIn = true;
        }catch(e){
            console.log("Not logged in, waiting 5 seconds");
            await sleep(5000);
        }
    }
    console.log("Logged in"); 
}

Highlights:scrollDown() uses page.mouse.wheel() to lazily load more job cards
• Recursive pagination keeps memory usage low
• Smart detection of login completion via avatar presence

4. JobCardService.ts – AI-Powered Job Filtering

const answer = await ChatGptHelper.sendText(
    'gpt-4.1-nano', 
    `PROFILE: ${DEFINES.ABOUT_ME}\n\nBased on my profile, answer if this job is a good fit for me. Return only the "YES" or "NO", without any other text.`
);

if(answer?.toLocaleLowerCase().includes('yes')){
    console.log('🟢 This job is a good fit');
    await easyApplyButton.click();
    // ... proceed with application
}else{
    console.log('🔴 This job is not a good fit');
}

Smart Features:Two-stage ChatGPT prompts: Quick YES/NO filtering + detailed question answering
Rate limit detection: Monitors for "Easy Apply limit reached" messages
Automatic cooldown: Sleeps for 1 hour when limits are hit

5. ApplyService.ts – Intelligent Form Automation

// Extract question text
const labelHandle = await q.$('label');
const question = (await (await labelHandle.getProperty('innerText')).jsonValue()).trim();

// Get AI-powered answer
let answer = await ChatGptHelper.sendText(
    'gpt-4.1-nano', 
    `PROFILE: ${DEFINES.ABOUT_ME}\n\n` + 
    `ROLE DESCRIPTION: ${this.jobCardService.about}\n\n` + 
    `Based on my profile, answer the following question: ${question}\n\n` + 
    `Return only the answer, without any other text.`
);

// Handle different input types
const textInput = await q.$('input[type="text"], input[type="email"], input[type="tel"]');
if (textInput) {
    await textInput.click({ clickCount: 3 });
    await textInput.type(answer, { delay: 50 });
}

Advanced Capabilities:Universal input handling: Text, email, phone, date, file uploads, selects, radios, checkboxes
Smart type coercion: Strips non-digits for numeric fields
Error recovery: Detects validation errors and discards problematic applications
Multi-step navigation: Handles complex application flows with Next/Review buttons

6. ChatGptHelper.ts – Clean API Wrapper

import OpenAI from 'openai';

const openai = new OpenAI();

export default class ChatGptHelper {
    static async sendText(model: OpenAI.Chat.ChatModel = 'gpt-4o', content: string) {
        const completion = await openai.chat.completions.create({
            model,
            messages: [{ role: 'user', content }],
        });

        console.log(`🤖 Response from ChatGPT: ${completion.choices[0].message.content}`);
        return completion.choices[0].message.content;
    }
}

Pro Tip: Easily swap between models (gpt-4o, gpt-4-turbo, gpt-3.5-turbo) by changing the parameter.


Prompt Engineering Strategy

Quick Fit Assessment

PROFILE: 

Based on my profile, answer if this job is a good fit for me. 
Return only the "YES" or "NO", without any other text.

Detailed Question Answering

PROFILE: 

ROLE DESCRIPTION: 

Based on my profile, answer the following question: 

If you don't know the exact answer, return what you think is the best answer for this role.
Return only the answer, without any other text.

Design Principles:Deterministic prompts keep token costs low
Single-purpose queries improve accuracy
Contextual awareness with both profile and job description


Getting Started

Installation & Setup

# Clone the repository
git clone https://github.com/samuelfaj/linkedin-jobs-applier
cd linkedin-jobs-applier

# Install dependencies
bun install  # or npm install

# Set up environment
export OPENAI_API_KEY=sk-your-openai-key-here
export RESUME_PATH=/path/to/your/resume.pdf  # Optional

# Create your profile
echo "Your professional background, skills, and preferences..." > about-me.txt

# Run the bot
bun run src/index.ts

First Run Experience

  1. Chrome launches → Complete LinkedIn login manually
  2. Monitor progress → Watch the console for real-time updates
  3. Grab coffee → The bot takes over automatically

Customization Options

Task How to Customize
Change job search criteria Edit DEFINES.JOB_LINK with your LinkedIn search URL
Adjust response tone Modify prompts in ApplyService.ts
Add cover letter uploads Extend the file upload handling block
Switch to headless mode Change headless: true in PuppeteerService.ts
Modify AI model Update the model parameter in ChatGptHelper.ts calls

Important Considerations

Ethics & Compliance

LinkedIn actively detects automation—use VPNs, realistic delays, and moderate volume
Always review final submissions—AI can occasionally hallucinate salary expectations
Keep about-me.txt current with your latest skills and projects

Technical Limitations

Rate limits apply—LinkedIn restricts Easy Apply submissions per day
Site changes—CSS selectors may need updates as LinkedIn evolves
AI accuracy—Review generated responses for appropriateness


Future Enhancements

Local AI integration (e.g., Ollama + Llama 3) to reduce API costs
Gmail API integration for tracking recruiter responses
Analytics dashboard showing application success rates
A/B testing for prompt optimization
Mobile notifications for application status updates


Conclusion

This project demonstrates the power of combining Bun's speed, Puppeteer's flexibility, and ChatGPT's intelligence to automate tedious job application processes. What used to take 4+ hours of manual work now runs as a background script.

The key insight? AI excels at context-aware decision making, while traditional automation handles the mechanical interactions. By combining both approaches, we create a system that's both intelligent and efficient.

Whether you're building a small prototype or a large-scale automation solution, the core principles remain: keep your services cohesive, separate cross-cutting concerns, and leverage the advantages of strongly typed languages for more reliable, maintainable code. A purposeful approach to structuring your automation logic is the key to scaling smoothly and maintaining long-term effectiveness.

Remember: Use this responsibly, respect platform terms of service, and always maintain human oversight of your job applications.

Resources:GitHub Repository
Bun Documentation
Puppeteer Guides
OpenAI API Reference

Happy job hunting!


Resume

Experience

  • SecurityScoreCard

    Nov. 2023 - Present

    New York, United States

    Senior Software Engineer

    I joined SecurityScorecard, a leading organization with over 400 employees, as a Senior Full Stack Software Engineer. My role spans across developing new systems, maintaining and refactoring legacy solutions, and ensuring they meet the company's high standards of performance, scalability, and reliability.

    I work across the entire stack, contributing to both frontend and backend development while also collaborating directly on infrastructure-related tasks, leveraging cloud computing technologies to optimize and scale our systems. This broad scope of responsibilities allows me to ensure seamless integration between user-facing applications and underlying systems architecture.

    Additionally, I collaborate closely with diverse teams across the organization, aligning technical implementation with strategic business objectives. Through my work, I aim to deliver innovative and robust solutions that enhance SecurityScorecard's offerings and support its mission to provide world-class cybersecurity insights.

    Technologies Used:

    Node.js Terraform React Typescript AWS Playwright and Cypress