A comprehensive ATS (Applicant Tracking System) scoring engine with multi-LLM provider support.
- 5-Layer Output Structure: Machine-readable scores, recruiter explanations, audit evidence, workflow decisions, and candidate feedback
- Multi-LLM Support: OpenAI, Anthropic, Azure OpenAI, Google Gemini, Groq, OpenRouter, and custom OpenAI-compatible APIs
- LLM Optional: Works with rule-based scoring when no LLM is configured
- Batch Processing: Process 1-1000 candidates per batch with concurrency control
- Rule-Based Analysis: ATS parseability, formatting consistency, and structure analysis
- LLM-Enhanced Scoring: Semantic matching, skills verification, and experience alignment
- TypeScript First: Full type definitions included
flowchart TB
subgraph Input["📥 Input Layer"]
Single["Single Candidate<br/>scoreCandidate()"]
Batch["Batch Processing<br/>scoreCandidateBatch()<br/>(1-1000 candidates)"]
end
subgraph Extraction["📄 Extraction Layer"]
PDF["PDF Parser"]
DOCX["DOCX Parser"]
TXT["Text Extractor"]
Normalize["Text Normalizer"]
end
subgraph RuleBased["⚙️ Rule-Based Analysis"]
Parse["Parseability<br/>Analyzer"]
Format["Formatting<br/>Analyzer"]
Struct["Structure<br/>Analyzer"]
end
subgraph LLMLayer["🤖 LLM Analysis (Optional)"]
LLMCheck{{"LLM Config<br/>Provided?"}}
OpenAI["OpenAI<br/>GPT-4o"]
Anthropic["Anthropic<br/>Claude"]
Azure["Azure<br/>OpenAI"]
Google["Google<br/>Gemini"]
Groq["Groq<br/>Llama"]
OpenRouter["OpenRouter"]
Custom["Custom API"]
Semantic["Semantic<br/>Analysis"]
end
subgraph Validation["✅ Validation Layer"]
Validate["LLM Output<br/>Validator"]
RuleOnly["Rule-Based<br/>Fallback"]
end
subgraph Output["📊 5-Layer Output"]
Core["Layer 1: Core ATS<br/>(Machine-readable)"]
Explain["Layer 2: Explainability<br/>(Recruiter-facing)"]
Evidence["Layer 3: Evidence<br/>(Audit-grade)"]
Decision["Layer 4: Decision<br/>(Workflow-level)"]
Feedback["Layer 5: Feedback<br/>(Candidate-facing)"]
end
subgraph BatchOutput["📈 Batch Output"]
Rankings["Ranked<br/>Candidates"]
Stats["Statistics<br/>& Distribution"]
end
Single --> Extraction
Batch --> Extraction
Extraction --> PDF & DOCX & TXT
PDF & DOCX & TXT --> Normalize
Normalize --> RuleBased
Parse & Format & Struct --> LLMCheck
LLMCheck -->|"Yes"| OpenAI & Anthropic & Azure & Google & Groq & OpenRouter & Custom
LLMCheck -->|"No"| RuleOnly
OpenAI & Anthropic & Azure & Google & Groq & OpenRouter & Custom --> Semantic
Semantic --> Validate
Validate --> Core
RuleOnly --> Core
Core --> Explain --> Evidence --> Decision --> Feedback
Feedback --> Rankings
Rankings --> Stats
npm install @pendent/ats-engine
# Install peer dependencies based on your LLM provider
npm install openai # For OpenAI/Azure/Google/Groq/OpenRouter
npm install @anthropic-ai/sdk # For Anthropicimport { scoreCandidate, createLLMConfig, createJobInput } from '@pendent/ats-engine';
import { readFileSync } from 'fs';
const result = await scoreCandidate({
candidate: {
name: 'John Doe',
email: 'john@example.com',
phone: '+1-555-1234',
linkedinUrl: 'https://linkedin.com/in/johndoe',
githubUrl: 'https://github.com/johndoe', // optional
portfolioUrl: 'https://johndoe.dev', // optional
resumeBuffer: readFileSync('resume.pdf'),
resumeMimeType: 'application/pdf',
},
job: createJobInput(
'job-123',
'Senior Software Engineer',
'Looking for a senior engineer with React and Node.js experience...',
{
mustHave: ['React', 'Node.js', 'TypeScript'],
niceToHave: ['AWS', 'Docker', 'Kubernetes'],
},
{
requiredExperienceYears: 5,
experienceLevel: 'SENIOR',
}
),
llmConfig: createLLMConfig('openai', process.env.OPENAI_API_KEY!),
options: {
includeEvidence: true,
includeCandidateFeedback: true,
},
});
// Access the 5-layer output
console.log(result.core); // Layer 1: Machine-readable scores
console.log(result.explanation); // Layer 2: Recruiter-facing summary
console.log(result.evidence); // Layer 3: Audit-grade evidence
console.log(result.decision); // Layer 4: Workflow decision
console.log(result.candidateFeedback); // Layer 5: Candidate feedbackimport { quickScore, createJobInput } from '@pendent/ats-engine';
import { readFileSync } from 'fs';
const result = await quickScore(
readFileSync('resume.pdf'),
'application/pdf',
createJobInput('job-123', 'Developer', 'Description here')
);
console.log(`Score: ${result.score}/100 (${result.grade})`);
console.log(`Decision: ${result.decisionBand}`);
console.log('Issues:', result.issues);Process multiple candidates efficiently with concurrency control and progress tracking:
import { scoreCandidateBatch, createLLMConfig, createJobInput } from '@pendent/ats-engine';
// Prepare candidates (add candidateId to each)
const candidates = yourCandidates.map((c, i) => ({
...c,
candidateId: `candidate-${i}`,
}));
// Process batch with optional LLM
const result = await scoreCandidateBatch({
candidates,
job: createJobInput('job-123', 'Senior Engineer', 'Job description...', {
mustHave: ['React', 'Node.js'],
niceToHave: ['AWS', 'Docker'],
}),
// LLM is optional - omit for rule-based only scoring
llmConfig: createLLMConfig('openai', process.env.OPENAI_API_KEY!),
batchOptions: {
concurrency: 10, // Process 10 at a time (max 50)
continueOnError: true, // Don't stop on individual failures
onProgress: (progress) => {
console.log(`Progress: ${progress.percentage}%`);
console.log(`${progress.succeeded}/${progress.total} successful`);
console.log(`ETA: ${progress.estimatedTimeRemaining}ms`);
},
},
});
// Access batch results
console.log('Summary:', result.summary);
// {
// totalCandidates: 500,
// successfulScores: 498,
// failedScores: 2,
// totalProcessingTimeMs: 45000,
// averageProcessingTimeMs: 90,
// llmUsed: true
// }
console.log('Top Candidates:', result.rankedCandidates.slice(0, 10));
// Sorted by score (highest first)
console.log('Statistics:', result.statistics);
// {
// averageScore: 68,
// medianScore: 70,
// highestScore: 94,
// lowestScore: 23,
// scoreDistribution: {
// strongMatch: 45,
// goodMatch: 120,
// moderateMatch: 180,
// weakMatch: 100,
// noMatch: 53
// }
// }
// Access individual results
for (const candidate of result.results) {
if (candidate.success) {
console.log(`${candidate.candidateName}: ${candidate.output.core.overallScore}`);
} else {
console.log(`${candidate.candidateName}: FAILED - ${candidate.error}`);
}
}Score candidates without any LLM configuration:
import { scoreCandidate, createJobInput } from '@pendent/ats-engine';
// No llmConfig = automatic rule-based scoring
const result = await scoreCandidate({
candidate: { /* ... */ },
job: createJobInput('job-123', 'Developer', 'Description'),
// llmConfig is omitted - uses rule-based scoring only
});
console.log(result.core.overallScore); // Rule-based score
console.log(result.metadata.llmUsed); // false{
overallScore: 72.4,
decisionBand: "GOOD_MATCH",
confidence: 0.91,
componentScores: {
skillsMatch: 82,
experienceRelevance: 68,
educationFit: 60,
keywordCoverage: 74,
seniorityFit: 70,
parseability: 88,
formatting: 90
},
missingCriticalSkills: ["Kafka", "System Design"],
matchedSkills: {
exact: ["Node.js", "PostgreSQL", "AWS"],
semantic: ["REST APIs", "Distributed systems"]
}
}{
summary: "Strong backend profile with good cloud exposure but missing streaming skills.",
strengths: [
"5+ years backend experience",
"Strong Node.js and database usage",
"AWS production exposure"
],
gaps: [
"No Kafka or event-driven systems",
"Limited system design examples"
],
riskFlags: [
"Single company for 6 years",
"No leadership experience mentioned"
],
keyHighlights: {
yearsOfExperience: 5,
topSkills: ["Node.js", "PostgreSQL", "AWS"],
educationLevel: "bachelors"
}
}{
skillEvidence: {
"Node.js": [
"Built REST APIs using Node.js and Express",
"Microservices written in Node.js"
],
"AWS": [
"Deployed services on EC2 and RDS"
]
},
experienceEvidence: [
{
claim: "Node.js",
evidence: "...built REST APIs using Node.js...",
confidence: 0.95
}
]
}{
recommendedAction: "REVIEW",
autoReject: false,
humanReviewRequired: true,
reasonCodes: ["MISSING_NON_CRITICAL_SKILLS"],
queuePriority: 2,
suggestedNextStep: "Verify missing skills with candidate",
autoRejectReasons: [], // Reasons if auto-rejected
failedFilters: [] // Filters that failed
}{
readinessLevel: "NEAR_READY",
positives: ["Strong JavaScript skills", "Good experience"],
improvementSuggestions: [
"Add examples of system design decisions",
"Mention experience with event-driven architectures"
],
skillsToHighlight: ["Kafka", "System Design"],
formatSuggestions: ["Add clear section headers"],
potentialScoreImprovement: 15
}The ATS engine supports extensive customization via the ATSConfig interface. See Configuration Documentation for full details.
import { scoreCandidate } from '@pendent/ats-engine';
const result = await scoreCandidate({
candidate: { /* ... */ },
job: { /* ... */ },
options: {
config: {
// Decision thresholds
passThreshold: 65,
passReviewThreshold: 80,
// Custom scoring weights (must sum to 1.0)
scoringWeights: {
skillsWeight: 0.35,
experienceWeight: 0.25,
domainWeight: 0.10,
semanticWeight: 0.10,
educationWeight: 0.05,
resumeQualityWeight: 0.15,
},
// Penalty settings
penalties: {
missingCriticalSkillPenalty: 8,
experienceGapPenaltyPerYear: 3,
maxTotalPenalty: 40,
},
// Auto-reject rules
autoRejectRules: {
employmentGapEnabled: true,
employmentGapMonths: 12,
jobHoppingEnabled: true,
jobHoppingCount: 4,
jobHoppingYears: 3,
},
// Advanced filters
advancedFilters: {
skillMatchThreshold: 60,
experienceTolerance: 2,
minEducationLevel: "bachelors",
educationEquivalence: true,
},
},
},
});
// Check auto-reject and filter results
console.log(result.decision.autoReject); // boolean
console.log(result.decision.autoRejectReasons); // string[]
console.log(result.decision.failedFilters); // string[]
console.log(result.core.appliedPenalties); // string[]const config = createLLMConfig('openai', 'sk-...', {
model: 'gpt-4o-mini',
temperature: 0.1,
});const config = createLLMConfig('anthropic', 'sk-ant-...', {
model: 'claude-sonnet-4-20250514',
});const config = createLLMConfig('azure', process.env.AZURE_API_KEY!, {
model: 'gpt-4o-mini',
azureEndpoint: 'https://your-resource.openai.azure.com',
azureDeployment: 'your-deployment-name',
});const config = createLLMConfig('google', process.env.GOOGLE_API_KEY!, {
model: 'gemini-1.5-pro', // or gemini-1.5-flash
});const config = createLLMConfig('groq', process.env.GROQ_API_KEY!, {
model: 'llama-3.1-70b-versatile',
});const config = createLLMConfig('openrouter', process.env.OPENROUTER_API_KEY!, {
model: 'anthropic/claude-3.5-sonnet',
});const config = createLLMConfig('custom', 'your-api-key', {
baseURL: 'https://api.your-provider.com/v1',
model: 'your-model',
headers: { 'X-Custom-Header': 'value' },
});interface CandidateInput {
// Required
name: string;
email: string;
phone: string;
linkedinUrl: string;
resumeBuffer: Buffer;
resumeMimeType: string;
// Optional
githubUrl?: string;
portfolioUrl?: string;
coverLetter?: string;
resumeFileName?: string;
}interface JobInput {
// Required
id: string;
title: string;
description: string;
// Optional
category?: string;
department?: string;
location?: string;
locationType?: 'REMOTE' | 'HYBRID' | 'ONSITE';
employmentType?: 'FULL_TIME' | 'PART_TIME' | 'CONTRACT' | 'INTERNSHIP' | 'FREELANCE';
experienceLevel?: 'FRESHER' | 'ENTRY' | 'MID' | 'SENIOR' | 'LEAD' | 'EXECUTIVE';
// Skills
mustHaveSkills?: string[];
niceToHaveSkills?: string[];
// Experience
requiredExperienceYears?: number;
preferredIndustries?: string[];
}| Component | Max Score | Description |
|---|---|---|
| Parseability | 25 | ATS-friendly layout |
| Formatting | 20 | Consistent formatting |
| Structure | 10 | Required sections present |
| Semantic | 30 | Keyword coverage (LLM) |
| Skills | 10 | Required skills verified (LLM) |
| Experience | 10 | Experience alignment (LLM) |
| Education | 10 | Education fit (LLM) |
| Total | 115 | (normalized to 100) |
| Grade | Score | Decision Band |
|---|---|---|
| A | 85+ | STRONG_MATCH |
| B | 70-84 | GOOD_MATCH |
| C | 55-69 | MODERATE_MATCH |
| D | 40-54 | WEAK_MATCH |
| F | <40 | NO_MATCH |
- PDF (
.pdf) - Word Documents (
.docx) - Plain Text (
.txt)
Private - All rights reserved.