ERPNext Syntax: Hooks (hooks.py) Hooks in hooks.py enable custom apps to extend Frappe/ERPNext functionality. Quick Reference doc_events - Document Lifecycle
In hooks.py
doc_events
{ "*" : { "after_insert" : "myapp.events.log_all_inserts" } , "Sales Invoice" : { "validate" : "myapp.events.si_validate" , "on_submit" : "myapp.events.si_on_submit" } }
In myapp/events.py
import frappe def si_validate ( doc , method = None ) : """doc = document object, method = event name""" if doc . grand_total < 0 : frappe . throw ( "Total cannot be negative" ) scheduler_events - Periodic Tasks
In hooks.py
scheduler_events
{ "daily" : [ "myapp.tasks.daily_cleanup" ] , "hourly_long" : [ "myapp.tasks.heavy_sync" ] , "cron" : { "0 9 * * 1-5" : [ "myapp.tasks.weekday_morning" ] } }
In myapp/tasks.py
def daily_cleanup ( ) : """No arguments - called automatically""" frappe . db . delete ( "Log" , { "creation" : [ "<" , one_month_ago ( ) ] } ) extend_bootinfo - Client Data Injection
In hooks.py
extend_bootinfo
"myapp.boot.extend_boot"
In myapp/boot.py
- def
- extend_boot
- (
- bootinfo
- )
- :
- """bootinfo = dict that goes to frappe.boot"""
- bootinfo
- .
- my_setting
- =
- frappe
- .
- get_single
- (
- "My Settings"
- )
- .
- value
- // Client-side
- console
- .
- log
- (
- frappe
- .
- boot
- .
- my_setting
- )
- ;
- Most Used doc_events
- Event
- When
- Use Case
- validate
- Before every save
- Validation, calculations
- on_update
- After every save
- Notifications, sync
- after_insert
- After new doc
- Creation-only actions
- on_submit
- After submit
- Ledger entries
- on_cancel
- After cancel
- Reverse entries
- on_trash
- Before delete
- Cleanup
- Complete list
-
- See
- doc-events.md
- Scheduler Event Types
- Event
- Frequency
- Queue/Timeout
- hourly
- Every hour
- default / 5 min
- daily
- Every day
- default / 5 min
- weekly
- Every week
- default / 5 min
- monthly
- Every month
- default / 5 min
- hourly_long
- Every hour
- long / 25 min
- daily_long
- Every day
- long / 25 min
- cron
- Custom timing
- default / 5 min
- Cron syntax and examples
- See scheduler-events.md Critical Rules 1. bench migrate after scheduler changes
REQUIRED - otherwise changes won't be picked up
bench --site sitename migrate 2. No commits in doc_events
❌ WRONG
def on_update ( doc , method = None ) : frappe . db . commit ( )
Breaks transaction
✅ CORRECT - Frappe commits automatically
def on_update ( doc , method = None ) : update_related_docs ( doc ) 3. Changes after on_update via db_set
❌ WRONG - change is lost
def on_update ( doc , method = None ) : doc . status = "Processed"
✅ CORRECT
def on_update ( doc , method = None ) : frappe . db . set_value ( doc . doctype , doc . name , "status" , "Processed" ) 4. Heavy tasks to _long queue
❌ WRONG - timeout after 5 min
scheduler_events
{ "daily" : [ "myapp.tasks.process_all_records" ]
May take 20 min
}
✅ CORRECT - 25 min timeout
scheduler_events
{ "daily_long" : [ "myapp.tasks.process_all_records" ] } 5. Tasks receive no arguments
❌ WRONG
def my_task ( some_arg ) : pass
✅ CORRECT
def my_task ( ) :
Fetch data inside the function
pass Cron Syntax Cheatsheet
│ │ │ │ │ │ │ │ │ └── Day of week (0-6, Sun=0) │ │ │ └──── Month (1-12) │ │ └────── Day of month (1-31) │ └──────── Hour (0-23) └────────── Minute (0-59) Pattern Meaning */5 * * * * Every 5 minutes 0 9 * * * Daily at 09:00 0 9 * * 1-5 Weekdays at 09:00 0 0 1 * * First day of month 0 17 * * 5 Friday at 17:00 doc_events vs Controller Hooks Aspect doc_events (hooks.py) Controller Methods Location hooks.py doctype/xxx/xxx.py Scope Hook OTHER doctypes Only OWN doctype Multiple handlers ✅ Yes (list) ❌ No Priority After controller First Wildcard ( * ) ✅ Yes ❌ No Use doc_events when : Hooking other apps' DocTypes from your custom app Reacting to ALL DocTypes (wildcard) Registering multiple handlers Use controller methods when : Working on your own DocType You want full lifecycle control Reference Files File Contents doc-events.md All document events, signatures, execution order scheduler-events.md Scheduler types, cron syntax, timeouts bootinfo.md extend_bootinfo, session hooks overrides.md Override and extend patterns permissions.md Permission hooks fixtures.md Fixtures configuration examples.md Complete hooks.py examples anti-patterns.md Mistakes and corrections Configuration Hooks Override DocType Controller
In hooks.py
override_doctype_class
{ "Sales Invoice" : "myapp.overrides.CustomSalesInvoice" }
In myapp/overrides.py
from erpnext . accounts . doctype . sales_invoice . sales_invoice import SalesInvoice class CustomSalesInvoice ( SalesInvoice ) : def validate ( self ) : super ( ) . validate ( )
CRITICAL: always call super()!
- self
- .
- custom_validation
- (
- )
- Warning
- Last installed app wins when multiple apps override the same DocType. Override Whitelisted Methods
In hooks.py
override_whitelisted_methods
{ "frappe.client.get_count" : "myapp.overrides.custom_get_count" }
Method signature MUST be identical to original!
def custom_get_count ( doctype , filters = None , debug = False , cache = False ) :
Custom implementation
return frappe . db . count ( doctype , filters ) Permission Hooks
In hooks.py
permission_query_conditions
{ "Sales Invoice" : "myapp.permissions.si_query_conditions" } has_permission = { "Sales Invoice" : "myapp.permissions.si_has_permission" }
In myapp/permissions.py
def si_query_conditions ( user ) : """Returns SQL WHERE fragment for list filtering""" if not user : user = frappe . session . user if "Sales Manager" in frappe . get_roles ( user ) : return ""
No restrictions
return
f"tabSales Invoice.owner =
{
frappe
.
db
.
escape
(
user
)
}
"
def
si_has_permission
(
doc
,
user
=
None
,
permission_type
=
None
)
:
"""Document-level permission check"""
if
permission_type
==
"write"
and
doc
.
status
==
"Closed"
:
return
False
return
None
Fallback to default
Note : permission_query_conditions only works with get_list , NOT with get_all ! Fixtures
In hooks.py
fixtures
[ { "dt" : "Custom Field" , "filters" : [ [ "module" , "=" , "My App" ] ] } , { "dt" : "Property Setter" , "filters" : [ [ "module" , "=" , "My App" ] ] } , { "dt" : "Role" , "filters" : [ [ "name" , "like" , "MyApp%" ] ] } ]
Export fixtures to JSON
bench --site sitename export-fixtures Asset Includes
In hooks.py
Desk (backend) assets
app_include_js
"/assets/myapp/js/myapp.min.js" app_include_css = "/assets/myapp/css/myapp.min.css"
Website/Portal assets
web_include_js
"/assets/myapp/js/web.min.js" web_include_css = "/assets/myapp/css/web.min.css"
Form script extensions
doctype_js
{ "Sales Invoice" : "public/js/sales_invoice.js" } Install/Migrate Hooks
In hooks.py
after_install
"myapp.setup.after_install" after_migrate = "myapp.setup.after_migrate"
In myapp/setup.py
- def
- after_install
- (
- )
- :
- create_default_roles
- (
- )
- def
- after_migrate
- (
- )
- :
- clear_custom_cache
- (
- )
- Complete Decision Tree
- What do you want to achieve?
- │
- ├─► REACT to document events from OTHER apps?
- │ └─► doc_events
- │
- ├─► Run PERIODIC tasks?
- │ └─► scheduler_events
- │ ├─► < 5 min → hourly/daily/weekly/monthly
- │ ├─► > 5 min → hourly_long/daily_long/etc.
- │ └─► Specific time → cron
- │
- ├─► Send DATA to CLIENT at page load?
- │ └─► extend_bootinfo
- │
- ├─► Modify CONTROLLER of existing DocType?
- │ ├─► Frappe v16+ → extend_doctype_class (recommended)
- │ └─► Frappe v14/v15 → override_doctype_class
- │
- ├─► Modify API ENDPOINT?
- │ └─► override_whitelisted_methods
- │
- ├─► Customize PERMISSIONS?
- │ ├─► List filtering → permission_query_conditions
- │ └─► Document-level → has_permission
- │
- ├─► EXPORT/IMPORT configuration?
- │ └─► fixtures
- │
- ├─► ADD JS/CSS to desk or portal?
- │ ├─► Desk → app_include_js/css
- │ ├─► Portal → web_include_js/css
- │ └─► Form specific → doctype_js
- │
- └─► SETUP on install/migrate?
- └─► after_install, after_migrate
- Version Differences
- Feature
- v14
- v15
- v16
- doc_events
- ✅
- ✅
- ✅
- scheduler_events
- ✅
- ✅
- ✅
- extend_bootinfo
- ✅
- ✅
- ✅
- override_doctype_class
- ✅
- ✅
- ✅
- extend_doctype_class
- ❌
- ❌
- ✅
- permission_query_conditions
- ✅
- ✅
- ✅
- has_permission
- ✅
- ✅
- ✅
- fixtures
- ✅
- ✅
- ✅
- Anti-Patterns Summary
- ❌ Wrong
- ✅ Correct
- frappe.db.commit()
- in handler
- Frappe commits automatically
- doc.field = x
- in on_update
- frappe.db.set_value()
- Heavy task in
- daily
- Use
- daily_long
- Change scheduler without migrate
- Always
- bench migrate
- Sensitive data in bootinfo
- Only public config
- Override without
- super()
- Always
- super().method()
- first
- get_all
- with permission_query
- Use
- get_list
- Fixtures without filters
- Filter by module/app
- Full anti-patterns
- See anti-patterns.md