I'm Samuel FajreldinesI am a specialist in the entire JavaScript and TypeScript ecosystem (including Node.js, React, Angular and Vue.js) I am expert in AI and in creating AI integrated solutions I am expert in DevOps and Serverless Architecture (AWS, Google Cloud and Azure) I am expert in PHP and its frameworks (such as Codeigniter and Laravel). |
Samuel FajreldinesI 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.
|
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.
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.
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 |
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
index.ts
loads your job search URL, reads your profile from about-me.txt
, and launches the browser.
LinkedInService.login()
opens LinkedIn's sign-in page and waits for manual login completion (detected by avatar appearance).
searchJobs()
navigates to your search URL, scrolls through job cards, and implements recursive pagination with &start=
offsets (25 jobs per page).
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
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.)
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.
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.
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.
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
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
// 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
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.
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.
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
# 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
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 |
• 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
• 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
• 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
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!
About Me
Since I was a child, I've always wanted to be an inventor. As I grew up, I specialized in information systems, an area which I fell in love with and live around it. I am a full-stack developer and work a lot with devops, i.e., I'm a kind of "jack-of-all-trades" in IT. Wherever there is something cool or new, you'll find me exploring and learning... I am passionate about life, family, and sports. I believe that true happiness can only be achieved by balancing these pillars. I am always looking for new challenges and learning opportunities, and would love to connect with other technology professionals to explore possibilities for collaboration. If you are looking for a dedicated and committed full-stack developer with a passion for excellence, please feel free to contact me. It would be a pleasure to talk with you! |
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