Skip to main content

Overview

ION Career automatically calculates a score for each job applicant based on their answers to screening questions. This helps recruiters quickly identify the most qualified candidates.
Scoring logic is implemented in ion_career/handlers.py:4-41

How Scoring Works

The scoring system uses a simple algorithm:
  1. Count the number of “Yes” answers to screening questions
  2. Calculate a score multiplier based on total questions
  3. Multiply to get a score out of 10

Score Calculation Formula

score_multiplier = 10 / len(qset.questions)
final_score = score_multiplier * total_yes_answers

Example Calculation

If a job has 5 screening questions and an applicant answers “Yes” to 4 of them:
  • Score multiplier = 10 / 5 = 2
  • Final score = 2 × 4 = 8 out of 10

Score Calculation Process

When a Job Applicant is created via the web form, the process_job_questions handler is triggered:
import json
import frappe

@frappe.whitelist()
def process_job_questions(doc, method):
    if not doc.custom_job_question_answers:
        return

    answers = json.loads(doc.custom_job_question_answers)

    job_opening = doc.job_title
    qset_name = frappe.db.get_value(
        "Job Opening",
        job_opening,
        "custom_job_question_set"
    )

    if not qset_name:
        return

    qset = frappe.get_doc("Job Question Set", qset_name)

    total_answered = 0

    for q in qset.questions:
        doc.append("custom_question_answers", {
            "question": q.question,
            "fieldname": q.fieldname,
            "answer": answers.get(q.fieldname),
            "job_opening": job_opening
        })

        if answers.get(q.fieldname) == "Yes":
            total_answered += 1

    score_multiplier = 10 / len(qset.questions)
    doc.custom_score = score_multiplier * total_answered

    doc.save(ignore_permissions=True)
Full implementation available in ion_career/handlers.py:4-41

Score Display

The score is stored in the custom_score field on the Job Applicant doctype.

Field Properties

custom_score
Data
Displays the calculated score for the applicant. This field is:
  • Read-only (users cannot manually edit)
  • Translatable
  • Inserted after applicant_rating
  • Not required
Defined in custom_field.json:59-115

Where to Find Scores

Scores are visible in:
  1. Job Applicant Form: In the main details section, after the applicant rating
  2. List View: Can be added as a column for quick comparison
  3. Reports: Available for filtering and sorting candidates

Score Usage

Ranking Candidates

Use scores to quickly identify top candidates:
// Example: Filter applicants with score >= 7
frappe.db.get_list('Job Applicant', {
    filters: {
        job_title: 'Software Engineer',
        custom_score: ['>=', 7]
    },
    fields: ['name', 'applicant_name', 'custom_score'],
    order_by: 'custom_score desc'
})

Setting Thresholds

Establish score thresholds for different actions:

9-10: Auto-Interview

Automatically schedule for interview

6-8: Review

Manual review by recruiter

0-5: Reject

Politely decline with templated email

Combining with Manual Rating

The score complements Frappe’s built-in applicant_rating field:
  • Custom Score: Objective measure based on screening questions
  • Applicant Rating: Subjective rating by recruiters after review
Use the custom score for initial filtering, then apply manual ratings during the interview process.

Answer Storage

Answers are stored in two formats:

1. JSON Format (Hidden)

The custom_job_question_answers field stores raw answers as JSON:
{
  "field_abc123": "Yes",
  "field_def456": "No",
  "field_ghi789": "Yes"
}

2. Table Format (Visible)

The custom_question_answers field displays answers in a readable table:
custom_question_answers
Table
Links to Job Applicant Question Answer child doctype. Shows:
  • Question text
  • Field name
  • Answer value
  • Associated job opening
Defined in custom_field.json:287-343

Validation with Scoring

The validate handler ensures required questions are answered before saving:
def validate(doc, method):
    if not doc.custom_job_question_answers:
        return

    answers = json.loads(doc.custom_job_question_answers)

    qset_name = frappe.db.get_value(
        "Job Opening",
        doc.job_opening,
        "custom_job_question_set"
    )

    qset = frappe.get_doc("Job Question Set", qset_name)

    for q in qset.questions:
        if q.required and q.fieldname not in answers:
            frappe.throw(
                f"Missing answer for required question: {q.question}"
            )
See implementation in ion_career/handlers.py:44-62
If a required question is missing, the validation will fail and the applicant record will not be created.

Real-World Examples

Example 1: Perfect Score

Question Set: Software Engineer Screening (5 questions)
  • Do you have 3+ years Python experience? → Yes
  • Are you familiar with Frappe Framework? → Yes
  • Can you work EST timezone? → Yes
  • Do you have experience with REST APIs? → Yes
  • Are you comfortable with Git? → Yes
Score: 10 / 10 ⭐

Example 2: Partial Match

Question Set: Remote Sales Position (4 questions)
  • Do you have sales experience? → Yes
  • Can you work remotely? → Yes
  • Do you have CRM experience? → No
  • Are you available for travel? → No
Score: 5 / 10

Example 3: Deal-Breaker Questions

Question Set: Licensed Professional (3 questions)
  • Do you have required certification? → No (required)
  • Are you authorized to work? → Yes (required)
  • Can you start immediately? → Yes
Result: Application rejected due to missing required answer

Customizing the Scoring Algorithm

You can modify the scoring logic to fit your needs:

Weighted Questions

Assign different weights to questions:
# Custom implementation example
weights = {
    'experience': 3,
    'certification': 2,
    'availability': 1
}

total_weight = sum(weights.values())
weighted_score = 0

for fieldname, weight in weights.items():
    if answers.get(fieldname) == "Yes":
        weighted_score += weight

final_score = (weighted_score / total_weight) * 10

Pass/Fail Threshold

Require minimum score to proceed:
if doc.custom_score < 6:
    doc.status = "Rejected"
else:
    doc.status = "Open"

Multi-Criteria Scoring

Combine multiple factors:
question_score = (yes_count / total_questions) * 10
experience_score = calculate_experience_score(doc.total_experience)
education_score = calculate_education_score(doc.qualifications)

final_score = (question_score * 0.5 + 
               experience_score * 0.3 + 
               education_score * 0.2)

Best Practices

  • Use questions that truly differentiate candidates
  • Avoid questions that most applicants will answer the same way
  • Balance “must-have” vs “nice-to-have” criteria
  • Track scores over time to understand distribution
  • Adjust thresholds based on hiring success rates
  • Don’t rely solely on scores—use as one input
  • Let applicants know screening questions affect ranking
  • Provide clear instructions on the web form
  • Send personalized rejections, not just automated ones
  • Regularly review which questions correlate with successful hires
  • Remove questions that don’t provide useful signal
  • Update question sets based on changing requirements

Reporting and Analytics

Use scores in reports to gain insights:
# Average score by job opening
frappe.db.sql("""
    SELECT job_title, AVG(CAST(custom_score AS DECIMAL)) as avg_score
    FROM `tabJob Applicant`
    WHERE custom_score IS NOT NULL
    GROUP BY job_title
""")

# Score distribution
frappe.db.sql("""
    SELECT 
        CASE 
            WHEN custom_score >= 9 THEN 'Excellent'
            WHEN custom_score >= 7 THEN 'Good'
            WHEN custom_score >= 5 THEN 'Average'
            ELSE 'Below Average'
        END as category,
        COUNT(*) as count
    FROM `tabJob Applicant`
    WHERE custom_score IS NOT NULL
    GROUP BY category
""")

Build docs developers (and LLMs) love