Skip to content

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,
};

Released under the Apache 2.0 License.