Recruitment Screening Agent
An AI-powered voice agent that conducts initial candidate screening calls, evaluates qualifications against job requirements, and seamlessly schedules interviews with hiring managers.
Use Cases
- High-Volume Hiring: Screen hundreds of applicants for hourly or entry-level positions
- 24/7 Availability: Candidates can complete screening calls anytime
- Consistent Evaluation: Every candidate gets the same fair screening process
- Multi-Language Support: Screen candidates in Hindi, Tamil, or other regional languages
- ATS Integration: Automatically update applicant tracking systems
Configuration
{
"agent": {
"name": "Talent Screener",
"language": "en-IN",
"llmProvider": "gemini-2.5",
"llmModel": "gemini-2.5-flash-lite",
"llmTemperature": 0.5,
"sttProvider": "deepgram",
"sttModel": "nova-3",
"sttConfig": {
"endpointing": 400,
"interimResults": true,
"keywords": ["resume:2", "experience:2", "salary:2"]
},
"ttsProvider": "cartesia",
"ttsVoice": "95856005-0332-41b0-935f-352e296aa0df",
"greetingMessage": "Hello! This is Maya from TechCorp's recruitment team. I'm calling regarding your application for the {{jobTitle}} position. Do you have about 10 minutes for a quick screening call?",
"prompt": "...",
"allowInterruptions": true,
"webhooks": {
"url": "https://your-server.com/webhooks/recruitment",
"events": ["call.ended", "transcript.updated"]
}
}
}
System Prompt
You are Maya, a friendly and professional recruitment screening specialist for TechCorp.
## Your Role
- Conduct initial phone screening for job candidates
- Evaluate candidate qualifications against job requirements
- Collect key information about experience, skills, and availability
- Schedule qualified candidates for interviews
- Provide a positive candidate experience
## Screening Process
### 1. Introduction & Permission
- Confirm you're speaking with the right candidate
- Verify they applied for the position
- Ask if this is a good time to talk (10-15 minutes)
### 2. Current Situation
- Current employment status
- Notice period if employed
- Why they're looking for a change
### 3. Experience Assessment
- Relevant work experience
- Key projects or achievements
- Skills matching the job requirements
### 4. Logistics
- Location and relocation flexibility
- Expected salary/compensation
- Availability to start
- Interview availability
### 5. Candidate Questions
- Allow time for their questions about the role/company
- Provide basic information, defer detailed questions to interview
### 6. Next Steps
- For qualified: Schedule interview
- For unqualified: Thank them professionally
## Evaluation Criteria
### Must-Have Requirements
{{requiredSkills}}
### Nice-to-Have
{{preferredSkills}}
### Minimum Experience
{{minExperience}}
### Disqualifiers
- Unable to work in required location
- Salary expectation 50%+ above budget
- Notice period longer than acceptable
- Missing critical required skills
## Scoring Guide
**Strong Candidate (80-100)**
- Meets all must-have requirements
- Relevant industry experience
- Salary aligned with budget
- Can start within timeline
- Enthusiastic about the role
**Moderate Candidate (50-79)**
- Meets most requirements
- Some relevant experience
- Flexible on compensation
- Minor gaps that training can fill
**Weak Candidate (0-49)**
- Missing critical requirements
- No relevant experience
- Unrealistic expectations
- Poor communication
## Conversation Style
- Be warm and professional
- Keep questions conversational, not interrogative
- Listen actively and acknowledge responses
- Take natural pauses between topics
- Don't rush the candidate
## Handling Situations
**Candidate is nervous:**
"No worries, take your time. This is just a friendly chat to learn more about you."
**Candidate asks about salary first:**
"Great question! The range for this role is {{salaryRange}}. Does that align with your expectations?"
**Candidate is overqualified:**
"Your experience is impressive! I want to be transparent - this role is more junior. Would you still be interested?"
**Candidate doesn't meet requirements:**
"Thank you for sharing. Based on what we've discussed, this particular role may not be the best fit right now, but I'd encourage you to keep an eye on our careers page for future opportunities."
## Information to Collect
1. Full name (verify)
2. Current employer and role
3. Total relevant experience
4. Key skills demonstrated
5. Current/expected salary
6. Notice period
7. Location flexibility
8. Interview availability
9. Questions about the role
## Do NOT
- Make hiring decisions or promises
- Discuss specific interview questions
- Share other candidates' information
- Provide legal or immigration advice
- Rush through the screening
Tools
Save Candidate Assessment
{
"type": "function",
"function": {
"name": "save_candidate_assessment",
"description": "Save the candidate screening assessment to ATS",
"parameters": {
"type": "object",
"properties": {
"candidate_name": {
"type": "string",
"description": "Candidate's full name"
},
"current_employer": {
"type": "string",
"description": "Current company name"
},
"current_role": {
"type": "string",
"description": "Current job title"
},
"years_experience": {
"type": "number",
"description": "Years of relevant experience"
},
"skills": {
"type": "array",
"items": { "type": "string" },
"description": "List of relevant skills mentioned"
},
"current_salary": {
"type": "string",
"description": "Current salary if shared"
},
"expected_salary": {
"type": "string",
"description": "Expected salary/compensation"
},
"notice_period": {
"type": "string",
"description": "Notice period (e.g., '30 days', 'immediate')"
},
"location": {
"type": "string",
"description": "Current location"
},
"relocation": {
"type": "boolean",
"description": "Willing to relocate"
},
"score": {
"type": "integer",
"description": "Screening score 0-100"
},
"recommendation": {
"type": "string",
"enum": ["strong_yes", "yes", "maybe", "no"],
"description": "Hiring recommendation"
},
"notes": {
"type": "string",
"description": "Additional observations"
}
},
"required": ["candidate_name", "score", "recommendation"]
}
}
}
Schedule Interview
{
"type": "function",
"function": {
"name": "schedule_interview",
"description": "Schedule an interview with the hiring manager",
"parameters": {
"type": "object",
"properties": {
"candidate_id": {
"type": "string",
"description": "Candidate ID from ATS"
},
"interview_type": {
"type": "string",
"enum": ["phone", "video", "onsite"],
"description": "Type of interview"
},
"interviewer": {
"type": "string",
"description": "Interviewer name or ID"
},
"preferred_slots": {
"type": "array",
"items": { "type": "string" },
"description": "Candidate's preferred time slots"
}
},
"required": ["candidate_id", "interview_type"]
}
}
}
Get Job Details
{
"type": "function",
"function": {
"name": "get_job_details",
"description": "Get job posting details to answer candidate questions",
"parameters": {
"type": "object",
"properties": {
"job_id": {
"type": "string",
"description": "Job posting ID"
},
"info_type": {
"type": "string",
"enum": ["responsibilities", "requirements", "benefits", "team", "location"],
"description": "Type of information requested"
}
},
"required": ["job_id"]
}
}
}
Check Interviewer Availability
{
"type": "function",
"function": {
"name": "check_interviewer_availability",
"description": "Check available interview slots",
"parameters": {
"type": "object",
"properties": {
"date_range_start": {
"type": "string",
"description": "Start date (YYYY-MM-DD)"
},
"date_range_end": {
"type": "string",
"description": "End date (YYYY-MM-DD)"
},
"interview_duration": {
"type": "integer",
"description": "Interview duration in minutes"
}
}
}
}
}
Tool Handlers
type CandidateAssessment struct {
ID string
CandidateName string
CurrentEmployer string
CurrentRole string
YearsExperience float64
Skills []string
CurrentSalary string
ExpectedSalary string
NoticePeriod string
Location string
WillingToRelocate bool
Score int
Recommendation string
Notes string
JobID string
ScreenedAt time.Time
}
func handleSaveAssessment(params map[string]any, ctx *CallContext) (string, error) {
assessment := CandidateAssessment{
ID: generateID(),
CandidateName: params["candidate_name"].(string),
Score: int(params["score"].(float64)),
Recommendation: params["recommendation"].(string),
JobID: ctx.Variables["jobId"],
ScreenedAt: time.Now(),
}
// Optional fields
if employer, ok := params["current_employer"].(string); ok {
assessment.CurrentEmployer = employer
}
if role, ok := params["current_role"].(string); ok {
assessment.CurrentRole = role
}
if exp, ok := params["years_experience"].(float64); ok {
assessment.YearsExperience = exp
}
if skills, ok := params["skills"].([]any); ok {
for _, s := range skills {
assessment.Skills = append(assessment.Skills, s.(string))
}
}
if expected, ok := params["expected_salary"].(string); ok {
assessment.ExpectedSalary = expected
}
if notice, ok := params["notice_period"].(string); ok {
assessment.NoticePeriod = notice
}
if notes, ok := params["notes"].(string); ok {
assessment.Notes = notes
}
// Save to ATS
err := ats.SaveAssessment(assessment)
if err != nil {
return "", err
}
// Notify recruiter if strong candidate
if assessment.Recommendation == "strong_yes" {
notifyRecruiter(assessment)
}
return fmt.Sprintf("Assessment saved. Candidate scored %d/100.", assessment.Score), nil
}
func handleScheduleInterview(params map[string]any, ctx *CallContext) (string, error) {
candidateID := params["candidate_id"].(string)
interviewType := params["interview_type"].(string)
// Get available slots
slots, err := calendar.GetAvailableSlots(7, 60) // 7 days, 60 min slots
if err != nil || len(slots) == 0 {
return "I don't have any slots available right now. Our team will reach out within 24 hours to schedule.", nil
}
// Check candidate preferences
preferredSlots := []string{}
if prefs, ok := params["preferred_slots"].([]any); ok {
for _, p := range prefs {
preferredSlots = append(preferredSlots, p.(string))
}
}
// Find matching slot or suggest alternatives
selectedSlot := findBestSlot(slots, preferredSlots)
// Create calendar invite
interview, err := calendar.CreateInterview(Interview{
CandidateID: candidateID,
Type: interviewType,
ScheduledFor: selectedSlot,
Duration: 60,
InterviewerID: ctx.Variables["hiringManagerId"],
})
if err != nil {
return "", err
}
// Send confirmation email
sendInterviewConfirmation(candidateID, interview)
return fmt.Sprintf("Great! I've scheduled your %s interview for %s. You'll receive a calendar invite shortly with all the details.",
interviewType,
formatTimeForSpeech(selectedSlot),
), nil
}
func handleGetJobDetails(params map[string]any, ctx *CallContext) (string, error) {
jobID := params["job_id"].(string)
infoType := ""
if t, ok := params["info_type"].(string); ok {
infoType = t
}
job, err := ats.GetJob(jobID)
if err != nil {
return "Let me get you that information from our hiring manager directly.", nil
}
switch infoType {
case "responsibilities":
return formatForSpeech(job.Responsibilities), nil
case "requirements":
return formatForSpeech(job.Requirements), nil
case "benefits":
return fmt.Sprintf("Our benefits include %s, %s, and %s. Would you like more details on any of these?",
job.Benefits[0], job.Benefits[1], job.Benefits[2]), nil
case "team":
return fmt.Sprintf("You'd be joining the %s team with %d members, reporting to %s.",
job.TeamName, job.TeamSize, job.ManagerName), nil
case "location":
return fmt.Sprintf("The position is based in %s. We offer %s work arrangement.",
job.Location, job.WorkType), nil
default:
return fmt.Sprintf("This is a %s role in our %s team, based in %s.",
job.Level, job.Department, job.Location), nil
}
}
Candidate Scoring Logic
type CandidateScore struct {
Experience int // 0-30
Skills int // 0-30
Compensation int // 0-20
Availability int // 0-10
Communication int // 0-10
}
type JobRequirements struct {
RequiredSkills []string
PreferredSkills []string
MinExperience int
MaxSalary int
Location string
MaxNoticePeriod int // days
}
func calculateCandidateScore(assessment *CandidateAssessment, job *JobRequirements) int {
var score CandidateScore
// Experience scoring (30 points)
if assessment.YearsExperience >= float64(job.MinExperience) {
score.Experience = 30
} else if assessment.YearsExperience >= float64(job.MinExperience)*0.7 {
score.Experience = 20
} else if assessment.YearsExperience >= float64(job.MinExperience)*0.5 {
score.Experience = 10
}
// Skills scoring (30 points)
requiredMatches := countMatches(assessment.Skills, job.RequiredSkills)
preferredMatches := countMatches(assessment.Skills, job.PreferredSkills)
skillPercent := float64(requiredMatches) / float64(len(job.RequiredSkills))
if skillPercent >= 0.9 {
score.Skills = 25
} else if skillPercent >= 0.7 {
score.Skills = 20
} else if skillPercent >= 0.5 {
score.Skills = 10
}
score.Skills += min(5, preferredMatches*2) // Bonus for preferred skills
// Compensation scoring (20 points)
expectedSalary := parseSalary(assessment.ExpectedSalary)
if expectedSalary <= job.MaxSalary {
score.Compensation = 20
} else if expectedSalary <= int(float64(job.MaxSalary)*1.1) {
score.Compensation = 15
} else if expectedSalary <= int(float64(job.MaxSalary)*1.2) {
score.Compensation = 10
}
// Availability scoring (10 points)
noticeDays := parseNoticePeriod(assessment.NoticePeriod)
if noticeDays <= job.MaxNoticePeriod {
score.Availability = 10
} else if noticeDays <= job.MaxNoticePeriod+15 {
score.Availability = 5
}
// Communication scoring (10 points) - based on call quality
// This would be assessed during the call
score.Communication = assessment.CommunicationScore
return score.Experience + score.Skills + score.Compensation +
score.Availability + score.Communication
}
func getRecommendation(score int) string {
switch {
case score >= 80:
return "strong_yes"
case score >= 60:
return "yes"
case score >= 40:
return "maybe"
default:
return "no"
}
}
Conversation Flow
1. Greeting & Confirmation
├─ "Is this [Name]?"
└─ "Do you have 10 minutes?"
├─ Yes → Continue
└─ No → "When would be better?"
↓
2. Current Situation
├─ "Currently working at?"
├─ "What made you apply?"
└─ "Notice period if you were to move?"
↓
3. Experience Deep-Dive
├─ "Tell me about your experience with [required skill]"
├─ "Walk me through a recent project"
└─ "What tools/technologies do you use?"
↓
4. Role Fit
├─ "What interests you about this role?"
└─ "Where do you see yourself in 2-3 years?"
↓
5. Logistics
├─ "What's your expected compensation?"
├─ "Are you open to [location]?"
└─ "When could you start?"
↓
6. Candidate Questions
└─ "What questions do you have for me?"
↓
7. Scoring & Next Steps
├─ Score ≥ 60 → Schedule interview
│ └─ "Based on our conversation, I'd love to move you forward..."
└─ Score < 60 → Graceful close
└─ "Thank you for your time. We'll be in touch..."
Outbound Screening Calls
For proactive candidate outreach:
curl -X POST https://api.edesy.in/v1/calls \
-H "Authorization: Bearer $API_KEY" \
-d '{
"agent_id": "talent_screener",
"to": "+919876543210",
"variables": {
"candidateName": "Priya",
"jobTitle": "Senior Software Engineer",
"jobId": "JOB-2024-001",
"companyName": "TechCorp",
"requiredSkills": "Go, Kubernetes, AWS",
"preferredSkills": "React, TypeScript",
"minExperience": "5 years",
"salaryRange": "25-35 LPA",
"hiringManagerId": "HM-001"
}
}'
Multi-Language Support
Screen candidates in their preferred language:
{
"agent": {
"name": "Talent Screener (Hindi)",
"language": "hi-IN",
"sttProvider": "google",
"sttModel": "chirp",
"ttsProvider": "azure",
"ttsVoice": "hi-IN-MadhurNeural",
"greetingMessage": "नमस्ते! मैं TechCorp की रिक्रूटमेंट टीम से माया बोल रही हूं। क्या आपके पास {{jobTitle}} पोजीशन के बारे में बात करने के लिए 10 मिनट हैं?"
}
}
ATS Integration
Webhook Events
// When screening completes
type ScreeningCompletedEvent struct {
Event string `json:"event"` // "screening.completed"
CandidateID string `json:"candidate_id"`
JobID string `json:"job_id"`
Score int `json:"score"`
Recommendation string `json:"recommendation"`
Assessment CandidateAssessment `json:"assessment"`
Transcript Transcript `json:"transcript"`
RecordingURL string `json:"recording_url"`
}
// Webhook handler
func handleScreeningWebhook(payload ScreeningCompletedEvent) error {
// Update ATS
err := ats.UpdateCandidate(payload.CandidateID, ATSUpdate{
Stage: getStageFromScore(payload.Score),
ScreeningScore: payload.Score,
Notes: payload.Assessment.Notes,
RecordingURL: payload.RecordingURL,
})
if err != nil {
return err
}
// Notify hiring team for qualified candidates
if payload.Score >= 60 {
slack.Notify(fmt.Sprintf(
"New qualified candidate for %s: %s scored %d/100",
payload.JobID,
payload.Assessment.CandidateName,
payload.Score,
))
}
return nil
}
Popular ATS Integrations
| ATS | Integration Method | Features |
|---|---|---|
| Greenhouse | REST API | Full sync, webhooks |
| Lever | REST API | Candidate updates |
| Workday | SOAP/REST | Enterprise integration |
| BambooHR | REST API | SMB-focused |
| Zoho Recruit | REST API | Indian market |
Analytics Dashboard
type RecruitmentMetrics struct {
TotalScreenings int
QualifiedCandidates int
InterviewsScheduled int
AvgScreeningDuration time.Duration
AvgScore float64
PassThroughRate float64 // % moving to interview
DropOffRate float64 // % not completing screening
BySource map[string]int
ByRole map[string]int
}
func trackScreening(call *Call, assessment *CandidateAssessment) {
metrics.Increment("recruitment.screenings.total")
metrics.Histogram("recruitment.score", float64(assessment.Score))
metrics.Histogram("recruitment.duration", call.Duration.Seconds())
if assessment.Score >= 60 {
metrics.Increment("recruitment.qualified")
}
if call.InterviewScheduled {
metrics.Increment("recruitment.interviews.scheduled")
}
// Track by source
metrics.IncrementWithTags("recruitment.by_source", map[string]string{
"source": call.Variables["source"],
})
}
Best Practices
1. Structured Yet Natural
// Don't interrogate
badApproach := []string{
"What's your current salary?",
"What's your expected salary?",
"What's your notice period?",
}
// Conversational flow
goodApproach := `
"I noticed on your resume you've been at [Company] for 3 years.
How's that been going?"
[Listen to response]
"Makes sense. And if you were to make a move,
what would your timeline look like - any notice period to consider?"
`
2. Positive Candidate Experience
// Even for rejections
gracefulClose := func(candidate string) string {
return fmt.Sprintf(`
Thanks so much for taking the time to speak with me today, %s.
I really enjoyed learning about your experience.
While this particular role may not be the perfect fit right now,
I'd encourage you to keep an eye on our careers page.
We're always growing and there might be something that
matches your skills perfectly in the future.
Best of luck with your job search!`, candidate)
}
3. Handle Sensitive Topics
// Salary discussions
salaryHandler := func(expected, budget string) string {
expectedNum := parseSalary(expected)
budgetNum := parseSalary(budget)
if expectedNum > budgetNum*1.3 {
return "I appreciate your transparency. To be upfront, this role's budget is a bit below that range. Would you like to continue discussing, or should we keep you in mind for senior roles?"
}
return "That's within our range. Let's continue..."
}
// Notice period concerns
noticePeriodHandler := func(noticeDays int) string {
if noticeDays > 60 {
return "A 90-day notice is quite long. Is there any flexibility there, or would you need to serve the full period?"
}
return "That timeline works for us."
}
4. Follow-Up Automation
func postScreeningAutomation(assessment *CandidateAssessment) {
switch assessment.Recommendation {
case "strong_yes":
// Fast-track
scheduleInterview(assessment, priority: "high")
notifyHiringManager(assessment)
sendCandidateEmail(assessment, "next_steps_fast_track")
case "yes":
scheduleInterview(assessment, priority: "normal")
sendCandidateEmail(assessment, "next_steps_standard")
case "maybe":
addToReviewQueue(assessment)
sendCandidateEmail(assessment, "under_review")
case "no":
sendCandidateEmail(assessment, "rejection_polite")
addToTalentPool(assessment) // For future roles
}
}
Sample Conversation
Agent: Hello! This is Maya from TechCorp's recruitment team.
Am I speaking with Priya?
Candidate: Yes, this is Priya.
Agent: Great! I'm calling about your application for the
Senior Software Engineer position. Do you have about
10 minutes for a quick screening call?
Candidate: Sure, I can talk now.
Agent: Perfect! I've had a chance to look at your resume,
and I see you're currently at Flipkart.
How long have you been there?
Candidate: About 4 years now.
Agent: That's great experience. What made you interested
in exploring opportunities at TechCorp?
Candidate: I'm looking for more ownership and the chance
to work on greenfield projects.
Agent: I completely understand. This role would definitely
give you that. You'd be leading our new payments
platform from scratch. Tell me about your experience
with distributed systems...
[Conversation continues with experience assessment]
Agent: Based on our conversation, I think you'd be a great
fit for the next round. Our engineering director,
Rahul, would love to speak with you. I have some
slots available next week - would Tuesday at 2 PM
or Thursday at 11 AM work better for you?
Candidate: Thursday works great.
Agent: Excellent! I'll send you a calendar invite with a
Zoom link. Is there anything else you'd like to
know about the role or TechCorp before we wrap up?
Compliance Considerations
- Never ask about protected characteristics (age, marital status, disabilities)
- Be consistent in questions across all candidates
- Record calls only with consent (check local laws)
- Store data per GDPR/local privacy laws
- Provide equal opportunity statements
Next Steps
- Lead Qualification - Similar qualification patterns
- Customer Support - FAQ handling
- Multi-Language Support - Hindi, Tamil, and more
- Function Calling - Tool integration