How to prevent duplicate Job Applicant submissions and update existing old records

I want to prevent users from submitting a new Job Applicant if they already applied within the last 6 month.

  • If the last application is within 6 month → block new submission.
  • If older than 6 month → update the existing application instead of creating a new one.
  • If no existing record → create a new application.

I tried using a before_insert method in the Job Applicant DocType to check existing records, but it still creates duplicate entries when older than 6 month instead of updating the old record.
I also wrote a server-side API method (submit_or_update_job_applicant) to handle insert/update logic, but I’m unsure how to integrate it with the Web Form properly.

Hello :wave:,

You can easily add those logic in server side scripts. The question is how you will identify that user uniquely, according to me you can use entered email or phone number.

Now if a user ABC apply for the first time and will enter some email and phone number. First time the new entry will get created. Now after one week he applied again so you can do one thing before the new entry creation you have to check is this user email or phone number exists in any previous documents in a given range from 6th month ago to today.

If you not found then you can create a New entry else if found any entry so just update the data in that.

PS: you also add a validation like if that user is applying for the same role with in 6 month so then don’t create new entry and if that user is applying for different role then create a new entry.

When I use a server-side script, I get many errors related to import statements. Even after removing the import statements, the errors still appear, and I cannot submit the form. When I add test data through the console, errors also show up.Cell In[9], line 11
1 doc = frappe.get_doc({
2 “doctype”: “Job Applicant”,
3 “email_id”: “applicant@example.com”,
(…)
8 “resume_attachment”: “”
9 })
—> 11 doc.insert(ignore_permissions=True, ignore_mandatory=True)
12 frappe.db.commit()
13 print(f"Inserted Job Applicant: {doc.name}")

File ~/frappe-desk-bench/apps/frappe/frappe/model/document.py:309, in Document.insert(self, ignore_permissions, ignore_links, ignore_if_duplicate, ignore_mandatory, set_name, set_child_names)
306 self.validate_higher_perm_levels()
308 self.flags.in_insert = True
→ 309 self.run_before_save_methods()
310 self._validate()
311 self.set_docstatus()

File ~/frappe-desk-bench/apps/frappe/frappe/model/document.py:1136, in Document.run_before_save_methods(self)
1133 return
1135 if self._action == “save”:
→ 1136 self.run_method(“validate”)
1137 self.run_method(“before_save”)
1138 elif self._action == “submit”:

File ~/frappe-desk-bench/apps/frappe/frappe/model/document.py:1011, in Document.run_method(self, method, *args, **kwargs)
1009 self.run_notifications(method)
1010 run_webhooks(self, method)
→ 1011 run_server_script_for_doc_event(self, method)
1013 return out

File ~/frappe-desk-bench/apps/frappe/frappe/core/doctype/server_script/server_script_utils.py:42, in run_server_script_for_doc_event(doc, event)
39 if scripts:
40 # run all scripts for this doctype + event
41 for script_name in scripts:
—> 42 frappe.get_doc(“Server Script”, script_name).execute_doc(doc)

File ~/frappe-desk-bench/apps/frappe/frappe/core/doctype/server_script/server_script.py:169, in ServerScript.execute_doc(self, doc)
163 def execute_doc(self, doc: Document):
164 “”“Specific to Document Event triggered Server Scripts
165
166 Args:
167 doc (Document): Executes script with for a certain document’s events
168 “””
→ 169 safe_exec(
170 self.script,
171 _locals={“doc”: doc},
172 restrict_commit_rollback=True,
173 script_filename=self.name,
174 )

File ~/frappe-desk-bench/apps/frappe/frappe/utils/safe_exec.py:114, in safe_exec(script, _globals, _locals, restrict_commit_rollback, script_filename)
110 filename += f": {frappe.scrub(script_filename)}"
112 with safe_exec_flags(), patched_qb():
113 # execute script compiled by RestrictedPython
→ 114 exec(
115 compile_restricted(script, filename=filename, policy=FrappeTransformer),
116 exec_globals,
117 _locals,
118 )
120 return exec_globals, _locals

File : job_application:1

ImportError: import not found

In [10]:

I am creating a Job Application web form where user-submitted data is stored in the Job Applicant list. However, I am facing an issue: the backend before_insert script is supposed to update existing records older than 6 months based on email and phone number, but it is generating duplicate entries.

When I use the API method submit_or_update_job_applicant, it does not find the existing record; it does not update the record but creates a new record with updated data. This causes emails to be stored like this: tester8@example.com-2.

Thank you for sharing it, But !!

Do you have this code somewhere else on GitHub or in proper files so I can understand better. And I’ll help to complete this :white_check_mark:

Well you can control this thorugh the workflow i suggest.
create a new workflow with the status fields like New application Active applicant and old applciant you can name according to your need
then in role permissions add the permission who can addid the form in each statuus in 6 months status he can only update and after update it will goto active status and he cannot submit new make email unique

When the same email is used to reapply through the Web Form, instead of updating the existing Job Applicant record, a new document is created with a suffix (e.g., antria.jane@example.com-1, antria.jane@example.com-2, etc.).

However, when I test using the console and use the submit_or_update_job_applicant() method, it correctly updates the existing record instead of creating a new one.

The issue occurs specifically when submitting the Web Form: it always creates a new entry instead of updating the existing one.

@frappe.whitelist(allow_guest=True)
def submit_or_update_job_applicant(data):
try:
data = json.loads(data)
email = (data.get(“email_id”) or “”).strip().lower()
phone = (data.get(“phone_number”) or “”).strip()
designation = (data.get(“designation”) or “”).strip()
if not email or not phone or not designation:
return {“error”: “Email, phone number, and designation are required”}
six_months_ago = getdate(add_months(now_datetime(), -6))
# Find all job applicants with same email and phone
existing_apps = frappe.db.get_all(
“Job Applicant”,
filters={
“email_id”: email,
“phone_number”: phone
},
fields=[“name”, “designation”, “creation”],
order_by=“creation desc”
)
for app in existing_apps:
creation_date = getdate(app.creation)
if app.designation == designation:
if creation_date > six_months_ago:
# Applied for same role within 6 months – Block
return {
“error”: f"You have already applied for ‘{designation}’ within the last 6 months."
}
else:
# Applied for same role but over 6 months ago – Update
doc = frappe.get_doc(“Job Applicant”, app.name)
doc.reload() # Reload latest data from DB to avoid timestamp mismatch
doc.update(data)
doc.modified = frappe.utils.now_datetime() # ← Add this line
doc.save(ignore_permissions=True)
return {
“message”: f"Old application for ‘{designation}’ updated.",
“name”: doc.name
}
# Applied for different role or no record found – Create new entry
doc = frappe.get_doc({
“doctype”: “Job Applicant”,
**data
})
doc.insert(ignore_permissions=True)
return {“message”: “New Job Applicant created”, “name”: doc.name}
except Exception:
frappe.log_error(frappe.get_traceback(), “submit_or_update_job_applicant Exception”)

I also tried using the before_insert hook to update the existing record, but it ends up updating the old record and still creating a duplicate.

class JobApplicant(Document):
def before_insert(self):
email = (self.email_id or “”).strip().lower()
phone = (self.phone_number or “”).strip()
if not email or not phone:
frappe.throw(“Email and phone number are required.”)
six_months_ago = getdate(add_months(nowdate(), -6))
existing = frappe.db.get_all(“Job Applicant”, filters={
“email_id”: email,
“phone_number”: phone
}, fields=[“name”, “creation”], order_by=“creation desc”)
if existing:
existing_doc = frappe.get_doc(“Job Applicant”, existing[0][“name”])
if getdate(existing_doc.creation) >= six_months_ago:
frappe.throw(“You have already applied within the last 6 months.”)
else:
# Update fields
existing_doc.custom_experience = self.custom_experience
existing_doc.custom_skill_set = self.custom_skill_set
existing_doc.applicant_name = self.applicant_name
existing_doc.custom_current_salary = self.custom_current_salary
existing_doc.custom_expected_salary = self.custom_expected_salary
existing_doc.flags.ignore_permissions = True
existing_doc.save()
frappe.logger().info(f"[Job Applicant] Updated existing application: {existing_doc.name} | Skills: {self.custom_skill_set} | Experience: {self.custom_experience}")
# Prevent new record from being inserted
self.flags.ignore_insert = True
frappe.response[“http_status_code”] = 200
frappe.response[“message”] = “You already applied. Your old application was updated instead.”

Is there a correct way to prevent this duplication and ensure that the Web Form updates the existing record instead ,when user reapply after 6 months ?

frappe.ready(function () {
frappe.web_form.on(“after_save”, function (doc) {
frappe.call({
method: “custom_hrms.api.job_applicant.submit_or_update_job_applicant”,
args: {
data: JSON.stringify({
email_id: doc.email_id,
phone_number: doc.phone_number,
designation: doc.designation,
applicant_name: doc.applicant_name,
custom_skill_set: doc.custom_skill_set
})
},
callback: function (r) {
if (r.message) {
frappe.msgprint(r.message);
} else if (r.error) {
frappe.msgprint(r.error);
}
}
});
});
});