AI Hiring & Recruitment Agent
Automated recruitment pipeline with AI screening and human oversight
typescript
import { cronflow } from 'cronflow';
import { z } from 'zod';
// Types and Interfaces
interface Candidate {
id: string;
name: string;
email: string;
phone?: string;
resumeUrl: string;
appliedFor: string;
applicationDate: Date;
status:
| 'new'
| 'screening'
| 'coding_test'
| 'interview'
| 'background_check'
| 'reference_check'
| 'final_review'
| 'hired'
| 'rejected';
scores: {
resume?: number;
coding?: number;
interview?: number;
background?: number;
references?: number;
overall?: number;
};
flags: string[];
metadata: Record<string, any>;
}
interface JobRequirement {
position: string;
department: string;
level: 'junior' | 'mid' | 'senior' | 'lead';
requiredSkills: string[];
preferredSkills: string[];
experience: string;
education: string;
salary: { min: number; max: number };
}
interface ResumeAnalysis {
score: number;
matchedSkills: string[];
missingSkills: string[];
experience: {
totalYears: number;
relevantYears: number;
companies: string[];
};
education: {
degree: string;
institution: string;
relevant: boolean;
};
redFlags: string[];
strengths: string[];
recommendation: 'strong_match' | 'good_match' | 'weak_match' | 'no_match';
}
interface CodingTestResult {
score: number;
completionTime: number;
testCases: {
passed: number;
total: number;
};
codeQuality: {
readability: number;
efficiency: number;
bestPractices: number;
};
plagiarismCheck: {
suspicious: boolean;
confidence: number;
};
}
interface InterviewAnalysis {
sentiment: {
overall: 'positive' | 'neutral' | 'negative';
confidence: number;
emotions: Record<string, number>;
};
communication: {
clarity: number;
confidence: number;
professionalism: number;
};
technicalResponses: {
accuracy: number;
depth: number;
problemSolving: number;
};
culturalFit: {
score: number;
notes: string[];
};
}
// Webhook Schemas
const applicationSchema = z.object({
candidate: z.object({
name: z.string(),
email: z.string().email(),
phone: z.string().optional(),
resumeUrl: z.string().url(),
appliedFor: z.string(),
}),
jobId: z.string(),
source: z.string().optional(),
});
const codingTestSchema = z.object({
candidateId: z.string(),
testResults: z.object({
score: z.number().min(0).max(100),
completionTime: z.number(),
submissionUrl: z.string().url(),
testCases: z.object({
passed: z.number(),
total: z.number(),
}),
}),
});
const interviewRecordingSchema = z.object({
candidateId: z.string(),
recordingUrl: z.string().url(),
interviewType: z.enum(['phone', 'video', 'in_person']),
interviewerNotes: z.string().optional(),
duration: z.number(),
});
const backgroundCheckSchema = z.object({
candidateId: z.string(),
provider: z.string(),
results: z.object({
criminal: z.boolean(),
employment: z.boolean(),
education: z.boolean(),
credit: z.boolean().optional(),
references: z.boolean(),
}),
details: z.record(z.any()).optional(),
});
// Mock AI Functions (replace with real AI services)
async function analyzeResume(
resumeUrl: string,
jobRequirements: JobRequirement
): Promise<ResumeAnalysis> {
// Simulate AI processing time
await new Promise(resolve => setTimeout(resolve, 200));
// Mock resume analysis
const mockSkills = ['JavaScript', 'React', 'Node.js', 'Python', 'SQL', 'AWS'];
const matchedSkills = mockSkills.filter(() => Math.random() > 0.3);
const missingSkills = jobRequirements.requiredSkills.filter(
skill => !matchedSkills.includes(skill)
);
const score = Math.max(
20,
Math.min(
95,
(matchedSkills.length / jobRequirements.requiredSkills.length) * 80 +
Math.random() * 20
)
);
return {
score,
matchedSkills,
missingSkills,
experience: {
totalYears: Math.floor(Math.random() * 10) + 1,
relevantYears: Math.floor(Math.random() * 8) + 1,
companies: ['TechCorp', 'StartupXYZ', 'BigTech Inc.'],
},
education: {
degree: 'Bachelor of Computer Science',
institution: 'Tech University',
relevant: true,
},
redFlags: Math.random() > 0.8 ? ['Employment gap', 'Job hopping'] : [],
strengths: ['Strong technical background', 'Leadership experience'],
recommendation:
score > 75
? 'strong_match'
: score > 60
? 'good_match'
: score > 40
? 'weak_match'
: 'no_match',
};
}
async function analyzeCodingTest(
submissionUrl: string
): Promise<CodingTestResult> {
await new Promise(resolve => setTimeout(resolve, 300));
const passed = Math.floor(Math.random() * 10) + 5;
const total = 15;
const score = (passed / total) * 100;
return {
score,
completionTime: Math.floor(Math.random() * 7200) + 1800, // 30min - 2h
testCases: { passed, total },
codeQuality: {
readability: Math.random() * 40 + 60,
efficiency: Math.random() * 30 + 70,
bestPractices: Math.random() * 35 + 65,
},
plagiarismCheck: {
suspicious: Math.random() > 0.9,
confidence: Math.random() * 0.3 + 0.7,
},
};
}
async function analyzeInterview(
recordingUrl: string
): Promise<InterviewAnalysis> {
await new Promise(resolve => setTimeout(resolve, 1000)); // Longer processing for audio/video
return {
sentiment: {
overall: ['positive', 'neutral', 'negative'][
Math.floor(Math.random() * 3)
] as any,
confidence: Math.random() * 0.3 + 0.7,
emotions: {
confidence: Math.random(),
enthusiasm: Math.random(),
nervousness: Math.random() * 0.5,
professionalism: Math.random() * 0.3 + 0.7,
},
},
communication: {
clarity: Math.random() * 20 + 80,
confidence: Math.random() * 25 + 75,
professionalism: Math.random() * 15 + 85,
},
technicalResponses: {
accuracy: Math.random() * 30 + 70,
depth: Math.random() * 40 + 60,
problemSolving: Math.random() * 35 + 65,
},
culturalFit: {
score: Math.random() * 20 + 80,
notes: ['Team player', 'Growth mindset', 'Good communicator'],
},
};
}
async function conductBackgroundCheck(candidateId: string): Promise<any> {
await new Promise(resolve => setTimeout(resolve, 5000)); // Longer for background checks
return {
criminal: Math.random() > 0.05, // 95% pass rate
employment: Math.random() > 0.1, // 90% pass rate
education: Math.random() > 0.05, // 95% pass rate
credit: Math.random() > 0.2, // 80% pass rate
references: Math.random() > 0.15, // 85% pass rate
details: {
criminal_records: 'No records found',
employment_verification: 'All positions verified',
education_verification: 'Degree confirmed',
},
};
}
// Job requirements storage (mock database)
const jobRequirements: Record<string, JobRequirement> = {
'senior-fullstack-engineer': {
position: 'Senior Full Stack Engineer',
department: 'Engineering',
level: 'senior',
requiredSkills: ['JavaScript', 'React', 'Node.js', 'SQL', 'AWS'],
preferredSkills: ['TypeScript', 'GraphQL', 'Docker', 'Kubernetes'],
experience: '5+ years in full stack development',
education: "Bachelor's degree in Computer Science or equivalent",
salary: { min: 120000, max: 180000 },
},
'data-scientist': {
position: 'Data Scientist',
department: 'Data',
level: 'mid',
requiredSkills: ['Python', 'SQL', 'Machine Learning', 'Statistics'],
preferredSkills: ['TensorFlow', 'PyTorch', 'R', 'Spark'],
experience: '3+ years in data science',
education: "Master's degree in Data Science, Statistics, or related field",
salary: { min: 100000, max: 150000 },
},
};
// Candidate storage (in-memory for demo, use database in production)
const candidates: Map<string, Candidate> = new Map();
// Main Hiring Workflow
const hiringWorkflow = cronflow.define({
id: 'ai-hiring-recruitment',
name: 'AI Hiring & Recruitment Agent',
description:
'Automated recruitment pipeline with AI screening and human oversight',
hooks: {
onSuccess: (ctx, stepId) => {
if (!stepId) {
console.log('🎉 Hiring workflow completed successfully!');
console.log('📊 Final candidate status:', ctx.last);
}
},
onFailure: (ctx, stepId) => {
console.error(
`❌ Hiring workflow failed at step ${stepId}:`,
ctx.step_error
);
},
},
});
// 1. New Application Webhook
hiringWorkflow
.onWebhook('/webhooks/new-application', {
schema: applicationSchema,
})
.step('register-candidate', async ctx => {
const { candidate, jobId, source } = ctx.payload;
const candidateId = `candidate_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const newCandidate: Candidate = {
id: candidateId,
name: candidate.name,
email: candidate.email,
phone: candidate.phone,
resumeUrl: candidate.resumeUrl,
appliedFor: candidate.appliedFor,
applicationDate: new Date(),
status: 'new',
scores: {},
flags: [],
metadata: { jobId, source: source || 'direct' },
};
candidates.set(candidateId, newCandidate);
console.log(
`📥 New application received: ${candidate.name} for ${candidate.appliedFor}`
);
return {
candidateId,
candidate: newCandidate,
jobRequirements: jobRequirements[jobId] || null,
};
})
.step('ai-resume-screening', async ctx => {
const { candidateId, jobRequirements } = ctx.last;
const candidate = candidates.get(candidateId)!;
if (!jobRequirements) {
throw new Error(
`Job requirements not found for: ${candidate.appliedFor}`
);
}
console.log(`🤖 AI screening resume for: ${candidate.name}`);
const analysis = await analyzeResume(candidate.resumeUrl, jobRequirements);
// Update candidate
candidate.status = 'screening';
candidate.scores.resume = analysis.score;
candidate.flags.push(...analysis.redFlags);
candidate.metadata.resumeAnalysis = analysis;
return {
candidateId,
analysis,
passed: analysis.recommendation !== 'no_match' && analysis.score >= 60,
};
})
.if('resume-screening-passed', ctx => ctx.last.passed)
.step('send-coding-test', async ctx => {
const { candidateId } = ctx.last;
const candidate = candidates.get(candidateId)!;
console.log(`💻 Sending coding test to: ${candidate.name}`);
// Mock sending coding test email
const testLink = `https://codingtest.company.com/test/${candidateId}`;
candidate.status = 'coding_test';
candidate.metadata.codingTestSent = new Date();
candidate.metadata.testLink = testLink;
return {
candidateId,
testSent: true,
testLink,
expiresIn: '48 hours',
};
})
.action('notify-coding-test-sent', ctx => {
const { candidateId } = ctx.last;
const candidate = candidates.get(candidateId)!;
console.log(`📧 Coding test sent to ${candidate.email}`);
// Here you could send actual email notifications
})
.else()
.step('reject-candidate', async ctx => {
const { candidateId, analysis } = ctx.last;
const candidate = candidates.get(candidateId)!;
candidate.status = 'rejected';
candidate.metadata.rejectionReason = 'Failed resume screening';
candidate.metadata.rejectionDate = new Date();
console.log(
`❌ Candidate rejected: ${candidate.name} (Resume score: ${analysis.score})`
);
return {
candidateId,
rejected: true,
reason: 'Resume screening failed',
};
})
.endIf();
// 2. Coding Test Results Webhook
hiringWorkflow
.onWebhook('/webhooks/coding-test-results', {
schema: codingTestSchema,
})
.step('process-coding-results', async ctx => {
const { candidateId, testResults } = ctx.payload;
const candidate = candidates.get(candidateId);
if (!candidate) {
throw new Error(`Candidate not found: ${candidateId}`);
}
console.log(`🧪 Processing coding test for: ${candidate.name}`);
const analysis = await analyzeCodingTest(testResults.submissionUrl);
candidate.scores.coding = analysis.score;
candidate.metadata.codingAnalysis = analysis;
// Check for plagiarism
if (analysis.plagiarismCheck.suspicious) {
candidate.flags.push('Potential plagiarism detected');
}
return {
candidateId,
analysis,
passed: analysis.score >= 70 && !analysis.plagiarismCheck.suspicious,
};
})
.if('coding-test-passed', ctx => ctx.last.passed)
.step('schedule-interview', async ctx => {
const { candidateId } = ctx.last;
const candidate = candidates.get(candidateId)!;
candidate.status = 'interview';
candidate.metadata.interviewScheduled = new Date();
console.log(`📅 Scheduling interview for: ${candidate.name}`);
// Mock interview scheduling
const interviewSlot = {
date: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 1 week from now
interviewer: 'Sarah Johnson',
type: 'video',
duration: 60,
};
candidate.metadata.interview = interviewSlot;
return {
candidateId,
interviewScheduled: true,
interviewSlot,
};
})
.else()
.step('reject-coding-failure', async ctx => {
const { candidateId, analysis } = ctx.last;
const candidate = candidates.get(candidateId)!;
candidate.status = 'rejected';
candidate.metadata.rejectionReason = analysis.plagiarismCheck.suspicious
? 'Plagiarism detected in coding test'
: 'Failed coding test';
console.log(
`❌ Candidate rejected: ${candidate.name} (Coding score: ${analysis.score})`
);
return {
candidateId,
rejected: true,
reason: candidate.metadata.rejectionReason,
};
})
.endIf();
// 3. Interview Recording Analysis Webhook
hiringWorkflow
.onWebhook('/webhooks/interview-analysis', {
schema: interviewRecordingSchema,
})
.step('analyze-interview-recording', async ctx => {
const { candidateId, recordingUrl, interviewType, duration } = ctx.payload;
const candidate = candidates.get(candidateId);
if (!candidate) {
throw new Error(`Candidate not found: ${candidateId}`);
}
console.log(`🎤 Analyzing interview recording for: ${candidate.name}`);
const analysis = await analyzeInterview(recordingUrl);
// Calculate interview score
const interviewScore =
analysis.communication.clarity * 0.25 +
analysis.communication.confidence * 0.25 +
analysis.technicalResponses.accuracy * 0.3 +
analysis.culturalFit.score * 0.2;
candidate.scores.interview = interviewScore;
candidate.metadata.interviewAnalysis = analysis;
return {
candidateId,
analysis,
interviewScore,
passed: interviewScore >= 75 && analysis.sentiment.overall !== 'negative',
};
})
.if('interview-passed', ctx => ctx.last.passed)
.step('initiate-background-check', async ctx => {
const { candidateId } = ctx.last;
const candidate = candidates.get(candidateId)!;
candidate.status = 'background_check';
console.log(`🔍 Initiating background check for: ${candidate.name}`);
const backgroundResults = await conductBackgroundCheck(candidateId);
candidate.scores.background =
(Object.values(backgroundResults).filter(Boolean).length /
Object.values(backgroundResults).length) *
100;
candidate.metadata.backgroundCheck = backgroundResults;
return {
candidateId,
backgroundResults,
passed: Object.values(backgroundResults).every(Boolean),
};
})
.if('background-check-passed', ctx => ctx.last.passed)
.humanInTheLoop({
timeout: '72h',
description: 'Final hiring decision required',
onPause: (ctx, token) => {
const { candidateId } = ctx.last;
const candidate = candidates.get(candidateId)!;
console.log(`🛑 Human review required for: ${candidate.name}`);
console.log(`🔑 Approval token: ${token}`);
console.log(`📊 Candidate scores:`, candidate.scores);
// Send notification to hiring manager
console.log(`📧 Notification sent to hiring manager`);
},
})
.step('process-final-decision', async ctx => {
const { candidateId } = ctx.last;
const candidate = candidates.get(candidateId)!;
if (ctx.last.timedOut) {
candidate.status = 'rejected';
candidate.metadata.rejectionReason = 'Hiring decision timeout';
return {
candidateId,
decision: 'rejected',
reason: 'Decision timeout',
};
}
if (ctx.last.approved) {
candidate.status = 'hired';
candidate.metadata.hiredDate = new Date();
candidate.metadata.hiringManager = ctx.last.approvedBy;
console.log(`🎉 Candidate hired: ${candidate.name}`);
return {
candidateId,
decision: 'hired',
hiringManager: ctx.last.approvedBy,
};
} else {
candidate.status = 'rejected';
candidate.metadata.rejectionReason =
ctx.last.reason || 'Hiring manager decision';
console.log(`❌ Candidate rejected by hiring manager: ${candidate.name}`);
return {
candidateId,
decision: 'rejected',
reason: ctx.last.reason,
};
}
})
.else()
.step('reject-background-check', async ctx => {
const { candidateId, backgroundResults } = ctx.last;
const candidate = candidates.get(candidateId)!;
candidate.status = 'rejected';
candidate.metadata.rejectionReason = 'Failed background check';
const failedChecks = Object.entries(backgroundResults)
.filter(([_, passed]) => !passed)
.map(([check, _]) => check);
console.log(
`❌ Background check failed for: ${candidate.name} (${failedChecks.join(', ')})`
);
return {
candidateId,
rejected: true,
reason: `Failed background check: ${failedChecks.join(', ')}`,
};
})
.endIf()
.else()
.step('reject-interview-failure', async ctx => {
const { candidateId, interviewScore } = ctx.last;
const candidate = candidates.get(candidateId)!;
candidate.status = 'rejected';
candidate.metadata.rejectionReason = 'Failed interview';
console.log(
`❌ Interview failed for: ${candidate.name} (Score: ${interviewScore})`
);
return {
candidateId,
rejected: true,
reason: 'Interview performance below threshold',
};
})
.endIf();
// Bulk Application Processing Workflow
const bulkProcessingWorkflow = cronflow.define({
id: 'bulk-application-processing',
name: 'Bulk Application Processing',
description: 'Process 1000+ applications in minutes',
});
bulkProcessingWorkflow
.onWebhook('/webhooks/bulk-applications', {
schema: z.object({
applications: z.array(
z.object({
name: z.string(),
email: z.string().email(),
resumeUrl: z.string().url(),
appliedFor: z.string(),
})
),
jobId: z.string(),
batchId: z.string(),
}),
})
.step('validate-batch', async ctx => {
const { applications, jobId, batchId } = ctx.payload;
console.log(
`📦 Processing batch ${batchId}: ${applications.length} applications`
);
const jobReq = jobRequirements[jobId];
if (!jobReq) {
throw new Error(`Job requirements not found for: ${jobId}`);
}
return {
applications,
jobRequirements: jobReq,
batchId,
totalApplications: applications.length,
startTime: Date.now(),
};
})
.step('process-applications', async ctx => {
const { applications, jobRequirements, batchId, startTime } = ctx.last;
// Process applications in parallel using Promise.all
const applicationPromises = applications
.slice(0, 50)
.map((app: any, index: number) =>
processSingleApplication(app, jobRequirements, `${batchId}_${index}`)
);
const results = await Promise.all(applicationPromises);
return {
results,
startTime,
totalProcessed: results.length,
};
})
.step('aggregate-results', async ctx => {
const { results, startTime } = ctx.last;
const processed = results.filter((r: any) => r?.processed);
const passed = results.filter((r: any) => r?.passed);
const rejected = results.filter((r: any) => r?.rejected);
console.log(`📊 Batch processing complete:`);
console.log(` Total: ${results.length}`);
console.log(` Passed screening: ${passed.length}`);
console.log(` Rejected: ${rejected.length}`);
return {
totalProcessed: processed.length,
passedScreening: passed.length,
rejected: rejected.length,
processingTimeMs: Date.now() - startTime,
results: results,
};
})
.action('notify-bulk-complete', ctx => {
const stats = ctx.last;
console.log(`🚀 Bulk processing completed in ${stats.processingTimeMs}ms`);
console.log(
`📈 Success rate: ${((stats.passedScreening / stats.totalProcessed) * 100).toFixed(1)}%`
);
});
// Helper function for bulk processing
async function processSingleApplication(
application: any,
jobRequirements: JobRequirement,
candidateId: string
) {
try {
const candidate: Candidate = {
id: candidateId,
name: application.name,
email: application.email,
resumeUrl: application.resumeUrl,
appliedFor: application.appliedFor,
applicationDate: new Date(),
status: 'screening',
scores: {},
flags: [],
metadata: { bulk: true },
};
candidates.set(candidateId, candidate);
// AI resume screening
const analysis = await analyzeResume(
application.resumeUrl,
jobRequirements
);
candidate.scores.resume = analysis.score;
candidate.flags.push(...analysis.redFlags);
candidate.metadata.resumeAnalysis = analysis;
const passed =
analysis.recommendation !== 'no_match' && analysis.score >= 60;
if (passed) {
candidate.status = 'coding_test';
console.log(
`✅ ${application.name}: Passed screening (${analysis.score})`
);
} else {
candidate.status = 'rejected';
candidate.metadata.rejectionReason = 'Failed bulk resume screening';
console.log(
`❌ ${application.name}: Failed screening (${analysis.score})`
);
}
return {
candidateId,
name: application.name,
score: analysis.score,
passed,
rejected: !passed,
processed: true,
};
} catch (error) {
console.error(`Error processing ${application.name}:`, error);
return {
candidateId,
name: application.name,
error: error instanceof Error ? error.message : 'Unknown error',
processed: false,
};
}
}
// Analytics and Reporting Workflow
const analyticsWorkflow = cronflow.define({
id: 'hiring-analytics',
name: 'Hiring Analytics & Reporting',
description: 'Generate hiring metrics and reports',
});
analyticsWorkflow
.onSchedule('0 0 * * 1') // Every Monday at midnight
.step('generate-weekly-report', async ctx => {
const allCandidates = Array.from(candidates.values());
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const weeklyStats = {
totalApplications: allCandidates.filter(c => c.applicationDate > weekAgo)
.length,
resumeScreeningPassed: allCandidates.filter(
c => c.scores.resume && c.scores.resume >= 60
).length,
codingTestsPassed: allCandidates.filter(
c => c.scores.coding && c.scores.coding >= 70
).length,
interviewsPassed: allCandidates.filter(
c => c.scores.interview && c.scores.interview >= 75
).length,
hired: allCandidates.filter(c => c.status === 'hired').length,
rejected: allCandidates.filter(c => c.status === 'rejected').length,
conversionRate: 0,
averageProcessingTime: 0,
};
weeklyStats.conversionRate =
weeklyStats.totalApplications > 0
? (weeklyStats.hired / weeklyStats.totalApplications) * 100
: 0;
console.log('📊 Weekly Hiring Report:', weeklyStats);
return weeklyStats;
})
.action('send-report', ctx => {
// Here you could send the report via email, Slack, etc.
console.log('📧 Weekly report sent to hiring team');
});
console.log('🤖 AI Hiring & Recruitment Agent Starting...');
console.log('📥 Ready to process applications at:');
console.log(' POST /webhooks/new-application');
console.log(' POST /webhooks/coding-test-results');
console.log(' POST /webhooks/interview-analysis');
console.log(' POST /webhooks/bulk-applications');
console.log('');
console.log('🚀 Capabilities:');
console.log(' ✅ AI Resume Screening (<200ms per resume)');
console.log(' ✅ Automated Coding Test Analysis');
console.log(' ✅ Interview Sentiment Analysis');
console.log(' ✅ Background Check Integration');
console.log(' ✅ Human-in-the-loop Final Decisions');
console.log(' ✅ Bulk Processing (1000+ applications)');
export {
hiringWorkflow,
bulkProcessingWorkflow,
analyticsWorkflow,
candidates,
};