Replace per-doctype notification with FW Notification
Author: iamkhanraheelCreated Sep 15, 2026Updated Sep 15, 2026
Problem
Seven doctypes each independently reimplement the same "check a Settings toggle → render an Email Template → send" pipeline, spread across two different Settings doctypes:
| Doctype | Settings field(s) | Trigger |
|---|---|---|
| Leave Application | send_leave_notification, leave_approval_notification_template, leave_status_notification_template |
status change |
| Interview | send_interview_confirmation, interview_confirmation_template |
button click |
| Job Offer | hiring_sender_email (template picked ad hoc per send) |
button click |
| Expense Claim (#4895, not yet merged) | send_expense_notification, expense_claim_approval_notification_template, expense_claim_status_notification_template |
status change |
| Exit Interview | exit_questionnaire_notification_template |
button click (bulk) |
| Salary Slip | Payroll Settings.email_template |
on submit, or bulk button |
| Employee Reminders (birthday / anniversary / holiday) | send_birthday_reminders etc., hardcoded message, no Email Template |
scheduled (cron) |
Each one is a custom variant of the same idea: a toggle, a template, and hand-written Python to render and send. That's duplicated logic to maintain, duplicated bugs to fix, and no consistent way for a site to customize any of it.
Proposed direction
Frappe core already has a general-purpose mechanism for this: the Notification doctype. It covers every trigger shape these doctypes need via Notification.send(doc):
- Automatic: a persisted
Notificationrecord with a built-inevent(Save/Submit/Cancel/Value Change/Days Before, etc.). No code required. - Manual, fixed template: a persisted
Notificationwithevent="Method". A whitelisted handler does a permission check, then callsdoc.run_method("<name>").Document.run_method()always callsrun_notifications(method), which matchesNotificationrecords the same way lifecycle hooks do. - Manual, ad-hoc per-send choice: only needed where a user picks the template at send time (Job Offer). Build an unsaved
Notification(...)from the picks and call.send(doc)directly; it doesn't require persistence.
No flexibility is lost: only Job Offer ever had a per-send picker to begin with; everything else always sent the same fixed content.
Per-doctype mapping
| Shape | Doctypes | What gets written |
|---|---|---|
| Automatic | Leave Application, Expense Claim, Salary Slip (on-submit path) | Notification record(s), zero code |
| Manual, fixed template | Interview, Exit Interview, Salary Slip (bulk send) | Notification (event=Method) + permission check + run_method |
| Manual, ad-hoc | Job Offer | Unsaved Notification(...), .send(doc) |
What stays custom, and why
- Employee Reminders (birthday/anniversary/holiday): repeats every year by matching month and day (ignoring year), and broadcasts to other employees as recipients, neither of which
Notificationmodels. Worth a smaller independent fix: swap the hardcoded Python string for a real Email Template, keep the matching/recipient logic as-is.
Scope / rollout
- Leave Application: pure automatic, needs three
Notificationrecords (pending approval, status update, cancelled). (#5187) - Salary Slip: automatic on-submit path + manual bulk-send path.
- Exit Interview: manual, fixed template; recipient already resolved to a plain email field via
set_employee_email(), straightforward. - Expense Claim: automatic, same shape as Leave Application.
- Interview: manual, fixed template; recipient list comes from a child table (
Interview Detail.interviewer), the hardest of the "automatic/manual" group. - Job Offer: ad-hoc/ephemeral shape, needs the in-memory
Notificationtechnique. - Employee Reminders: out of scope for the
Notificationmigration; swap hardcoded message for an Email Template only.
Source: frappe/hrms