frappe-syntax-hooks-events — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited frappe-syntax-hooks-events (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
| Order | Event | Purpose | Can Raise? |
|---|---|---|---|
| 1 | before_insert | Set defaults before naming | YES |
| 2 | before_naming | Modify naming logic | YES |
| 3 | autoname | Set the name property | YES |
| 4 | before_validate | Auto-set missing values | YES |
| 5 | validate | Validation logic — throw to abort | YES |
| 6 | before_save | Final mutations before DB write | YES |
| 7 | db_insert | Internal — writes row to DB | — |
| 8 | after_insert | Post-insert logic (runs once ever) | YES |
| 9 | on_update | Post-save logic (runs on every save) | YES |
| 10 | on_change | Fires if any field value changed | YES |
| Order | Event | Purpose |
|---|---|---|
| 1 | before_validate | Auto-set missing values |
| 2 | validate | Validation logic — throw to abort |
| 3 | before_save | Final mutations before DB write |
| 4 | db_update | Internal — updates row in DB |
| 5 | on_update | Post-save logic |
| 6 | on_change | Fires if any field value changed |
| Order | Event | Purpose |
|---|---|---|
| 1 | before_validate | Auto-set missing values |
| 2 | validate | Validation logic |
| 3 | before_save | Final mutations before DB write |
| 4 | before_submit | Pre-submit logic — throw to abort |
| 5 | db_update | Internal — updates row in DB |
| 6 | on_submit | Post-submit logic (GL entries etc) |
| 7 | on_update | Post-save logic |
| 8 | on_change | Fires if any field value changed |
| Order | Event | Purpose |
|---|---|---|
| 1 | before_cancel | Pre-cancel validation |
| 2 | db_update | Internal — updates row in DB |
| 3 | on_cancel | Post-cancel logic (reverse GL etc) |
| 4 | on_change | Fires if any field value changed |
| Order | Event | Purpose |
|---|---|---|
| 1 | on_trash | Pre-delete cleanup |
| 2 | after_delete | Post-delete logic |
| Operation | Events (in order) |
|---|---|
| Rename | before_rename → after_rename |
| Amend | before_insert chain runs on the new amended doc |
| Update After Submit | before_update_after_submit → db_update → on_update_after_submit → on_change |
# hooks.py
doc_events = {
"Sales Invoice": {
"on_submit": "myapp.events.sales_invoice.on_submit",
"on_cancel": "myapp.events.sales_invoice.on_cancel",
},
"Purchase Order": {
"validate": "myapp.events.purchase_order.validate",
}
}doc_events = {
"*": {
"after_insert": "myapp.events.global_handler.after_insert_all",
"on_update": "myapp.events.global_handler.track_changes",
}
}ALWAYS use "*" (string with asterisk) as the key. This fires the handler for every DocType.
doc_events = {
"Sales Invoice": {
"on_submit": [
"myapp.events.accounting.create_gl_entries",
"myapp.events.notifications.send_invoice_email",
]
}
}# myapp/events/sales_invoice.py
def on_submit(doc, method=None):
"""
doc — the Document instance (e.g., Sales Invoice)
method — string name of the event (e.g., "on_submit"), or None
"""
if doc.grand_total > 10000:
frappe.sendmail(...)ALWAYS accept method as the second parameter (with default None). Frappe passes it automatically.
→ Use validate. ALWAYS raise frappe.throw() here to block invalid saves.
→ Use before_validate. This runs before validate, so your defaults are set before validation checks.
→ Use after_insert. This fires ONLY on insert, NEVER on subsequent saves.
→ Use on_update. This fires on both insert and save operations.
→ Use on_submit. NEVER create linked docs in validate — the document is not yet committed.
→ Use on_cancel. ALWAYS clean up GL entries, stock ledger entries, and linked docs here.
→ Use autoname in the controller, or before_naming for conditional logic.
→ Use on_trash. Raise frappe.throw() to block deletion.
→ Use before_update_after_submit for validation and on_update_after_submit for side effects.
→ Use on_change. This fires only when at least one field value differs from the DB state.
Both mechanisms trigger the SAME events. The difference is WHERE you register them.
| Aspect | Controller (class method) | doc_events (hooks.py) |
|---|---|---|
| Location | {doctype}.py controller file | hooks.py in your app |
| Use when | You OWN the DocType | You are EXTENDING another app's DocType |
| Execution | Runs first (controller) | Runs after controller method |
| Multiple apps | Only one controller per DocType | Multiple apps can register handlers |
ALWAYS use doc_events when hooking into a DocType you do NOT own. NEVER modify another app's controller file directly.
For a given event (e.g., validate):
def validate(self))doc_events handlers run in app installation order"*" handlers run after specific DocType handlersIn Frappe v16+, extend_doctype_class provides a cleaner alternative to doc_events for adding methods to existing DocTypes.
extend_doctype_class = {
"Sales Invoice": [
"myapp.overrides.sales_invoice.SalesInvoiceExtension"
]
}# myapp/overrides/sales_invoice.py
import frappe
class SalesInvoiceExtension:
def validate(self):
"""This is called as part of the controller chain."""
if self.grand_total < 0:
frappe.throw("Grand total cannot be negative")
def custom_method(self):
"""Custom methods are also available on the doc instance."""
return self.itemsextend_doctype_class over override_doctype_class in v16+ when multiple apps may extend the same DocType.class Final(App2Mixin, App1Mixin, Original).validate) run as part of the controller, NOT as separate doc_events handlers.Completely replaces the controller class. Use with extreme caution.
# hooks.py
override_doctype_class = {
"ToDo": "myapp.overrides.todo.CustomToDo"
}# myapp/overrides/todo.py
from frappe.desk.doctype.todo.todo import ToDo
class CustomToDo(ToDo):
def validate(self):
super().validate() # ALWAYS call super() to preserve original logic
# Your additions hereNEVER use override_doctype_class if extend_doctype_class is available (v16+). Only ONE app can override a DocType — last-installed app wins, silently breaking other apps.
When multiple apps register doc_events for the same DocType and event:
sites/{site}/site_config.json → installed_apps).override_doctype_class, the last-installed app wins (only one override applies).extend_doctype_class (v16+), all extensions stack cumulatively.All document events from before_validate through on_change run inside a single database transaction.
db_insert/db_update).after_insert, on_update, on_submit, on_cancel — all run BEFORE the transaction commits.after_delete runs after the DELETE statement but still within the request transaction.NEVER assume data is committed to DB inside any event handler. Other concurrent requests will NOT see your changes until the full request completes.
frappe.throw() to abort operations — NEVER use raise Exception.doc.name outside of autoname or before_naming.super().{event}() when overriding controller methods in subclasses.doc.save() inside validate or before_save — this causes infinite recursion.doc.flags.ignore_permissions = True explicitly if your hook needs to bypass permissions — NEVER assume hooks run as Administrator.validate — use after_insert or on_update with frappe.enqueue() instead.doc.flags to communicate between events in the same request (e.g., doc.flags.skip_notification = True).on_change for critical logic — it only fires when values actually differ from the database state.frappe-syntax-hooks-config — App-level hooks (scheduler, fixtures, permissions)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.