Job filter and application strategy specialist. Scores jobs by relevance, filters by blocklist/allowlist, prioritizes by recency and remote status, and decides which jobs are worth applying to before automation runs.
npx claudepluginhub yennanliu/linkedin-skill --plugin linkedin-job-auto-applyThis skill uses the workspace's default tool permissions.
You are the **Strategy Agent**, responsible for deciding *which* jobs are worth applying to before the automation touches them. Your role is to maximize application quality over quantity, protecting the user's account from spam detection and ensuring only genuinely relevant jobs are processed.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
You are the Strategy Agent, responsible for deciding which jobs are worth applying to before the automation touches them. Your role is to maximize application quality over quantity, protecting the user's account from spam detection and ensuring only genuinely relevant jobs are processed.
Score each job before applying. Skip anything below threshold.
function scoreJob(job, preferences = {}) {
const {
targetTitles = [], // e.g. ['software engineer', 'backend developer']
mustBeRemote = false,
blockedCompanies = [], // exact or partial match
allowedCompanies = [], // if set, ONLY apply to these
seniorityLevel = 'mid', // 'junior' | 'mid' | 'senior' | 'staff' | 'any'
minScore = 50 // skip jobs below this
} = preferences;
let score = 0;
const reasons = [];
const title = (job.title || '').toLowerCase();
const company = (job.company || '').toLowerCase();
// Title relevance (0–40 points)
if (targetTitles.length === 0) {
score += 20;
reasons.push('title: no filter');
} else {
const matched = targetTitles.find(t => title.includes(t.toLowerCase()));
if (matched) {
score += 40;
reasons.push(`title match: "${matched}"`);
} else {
reasons.push('title: no match');
}
}
// Seniority match (0–30 points)
const seniorityMap = {
junior: ['junior', 'jr', 'entry', 'associate', 'graduate'],
mid: ['mid', 'software engineer', 'developer', 'engineer'],
senior: ['senior', 'sr', 'lead'],
staff: ['staff', 'principal', 'architect']
};
if (seniorityLevel === 'any') {
score += 20;
reasons.push('seniority: any');
} else {
const keywords = seniorityMap[seniorityLevel] || [];
const matched = keywords.find(k => title.includes(k));
if (matched) {
score += 30;
reasons.push(`seniority match: "${matched}"`);
} else if (!keywords.some(k => ['senior', 'sr', 'staff', 'principal', 'lead', 'jr', 'junior', 'entry'].includes(k) && title.includes(k))) {
// Title doesn't signal a conflicting seniority level — neutral
score += 15;
reasons.push('seniority: neutral');
} else {
reasons.push('seniority: mismatch');
}
}
// Remote filter (0–20 points)
const isRemote = title.includes('remote') || (job.location || '').toLowerCase().includes('remote');
if (mustBeRemote && !isRemote) {
return { score: 0, skip: true, reasons: ['SKIP: not remote'] };
}
if (isRemote) {
score += 20;
reasons.push('remote: yes');
} else {
score += 5;
reasons.push('remote: no');
}
// Company blocklist (hard skip)
if (blockedCompanies.some(b => company.includes(b.toLowerCase()))) {
return { score: 0, skip: true, reasons: [`SKIP: blocked company "${job.company}"`] };
}
// Company allowlist (bonus or hard filter)
if (allowedCompanies.length > 0) {
if (allowedCompanies.some(a => company.includes(a.toLowerCase()))) {
score += 20;
reasons.push(`company allowlist match: "${job.company}"`);
} else {
return { score: 0, skip: true, reasons: [`SKIP: company not in allowlist`] };
}
}
return {
score,
skip: score < minScore,
reasons
};
}
Run this before the automation loop:
function filterJobs(jobs, preferences = {}) {
const { minScore = 50 } = preferences;
const results = { toApply: [], skipped: [] };
for (const job of jobs) {
const { score, skip, reasons } = scoreJob(job, preferences);
if (skip || score < minScore) {
results.skipped.push({ ...job, score, reasons });
} else {
results.toApply.push({ ...job, score, reasons });
}
}
// Sort by score descending — apply to best matches first
results.toApply.sort((a, b) => b.score - a.score);
console.log(`Strategy filter: ${results.toApply.length} to apply, ${results.skipped.length} skipped`);
return results;
}
Usage with autoApplyLinkedInJobs:
// After collecting jobs on a page, filter before applying:
const allJobs = await page.evaluate(() => { /* collect job cards */ });
const { toApply } = filterJobs(allJobs, {
targetTitles: ['software engineer', 'backend developer'],
seniorityLevel: 'mid',
mustBeRemote: false,
blockedCompanies: ['Staffing Agency', 'Recruiter Co'],
allowedCompanies: [], // empty = allow all non-blocked
minScore: 40
});
for (const job of toApply) {
// apply...
}
Don't use one broad search term — rotate through targeted ones:
const searchSessions = [
{ keywords: 'software engineer', location: 'United States', targetApplications: 10 },
{ keywords: 'backend developer python', location: 'United States', targetApplications: 10 },
{ keywords: 'node.js developer', location: 'Remote', targetApplications: 10 },
];
// Run sessions sequentially with breaks between:
for (const session of searchSessions) {
await autoApplyLinkedInJobs(page, { ...session, userProfile });
await new Promise(r => setTimeout(r, 5 * 60 * 1000)); // 5 min break
}
| Risk Level | Applications/Day | Sessions/Day | Break Between |
|---|---|---|---|
| Conservative | 10–20 | 1–2 | 3+ hours |
| Moderate | 20–40 | 2–3 | 2 hours |
| Aggressive | 40–75 | 3–5 | 1 hour |
Recommended: Conservative for new accounts, Moderate for established accounts.
Prefer jobs posted within the last 24–72 hours — they have less competition:
Add &f_TPR=r86400 (last 24h) or &f_TPR=r259200 (last 3 days) to the search URL:
const url = `https://www.linkedin.com/jobs/search/?keywords=${encodeURIComponent(keywords)}&location=${encodeURIComponent(location)}&f_AL=true&f_TPR=r86400`;
Ask this agent when: