Hooks
Hooks let an app extend Frappe and other apps without forking their code. Every app declares its hooks in hooks.py. This page covers both the concept and the full reference.
Why hooks
Frappe's philosophy: extend, don't fork. If you need to react when a Sales Invoice is submitted, don't edit ERPNext — write a hook in your own app. Updates to ERPNext don't conflict with your code, and your customisation survives every upgrade.
How hooks resolve
Multiple apps can hook the same event. Frappe runs them all in install order. None blocks the others unless one raises an exception — that aborts the operation for everyone. Design your hooks idempotent (safe to re-run) and lightweight.
Reference: All available hooks
Installation lifecycle
before_install = "my_app.install.before_install"
after_install = "my_app.install.after_install"
after_migrate = "my_app.install.after_migrate"
before_uninstall = "my_app.install.before_uninstall"
after_uninstall = "my_app.install.after_uninstall"
Document events
doc_events = {
"Sales Invoice": {
"validate": "my_app.custom.sales_invoice.validate",
"before_save": "my_app.custom.sales_invoice.before_save",
"on_update": "my_app.custom.sales_invoice.on_update",
"before_submit": "my_app.custom.sales_invoice.before_submit",
"on_submit": "my_app.custom.sales_invoice.on_submit",
"before_cancel": "my_app.custom.sales_invoice.before_cancel",
"on_cancel": "my_app.custom.sales_invoice.on_cancel",
"on_trash": "my_app.custom.sales_invoice.on_trash",
},
"*": {
"on_update": "my_app.audit.log_change", # use sparingly — fires on every DocType
}
}
Scheduler
scheduler_events = {
"all": ["my_app.tasks.all"], # Every minute
"hourly": ["my_app.tasks.hourly"],
"daily": ["my_app.tasks.daily"],
"weekly": ["my_app.tasks.weekly"],
"monthly": ["my_app.tasks.monthly"],
"cron": {
"0 9 * * 1-5": ["my_app.tasks.weekday_morning"]
}
}
Permissions
permission_query_conditions = {
"Sales Invoice": "my_app.permissions.sales_invoice_query",
}
has_permission = {
"Sales Invoice": "my_app.permissions.sales_invoice_has_permission",
}
DocType override
override_doctype_class = {
"Sales Invoice": "my_app.overrides.CustomSalesInvoice"
}
UI inclusion
app_include_js = "my_app.bundle.js" # Desk only
app_include_css = "my_app.bundle.css"
web_include_js = "my_portal.bundle.js" # Portal only
web_include_css = "my_portal.bundle.css"
doctype_js = {
"Customer": "public/js/customer.js"
}
doctype_list_js = {
"Sales Invoice": "public/js/sales_invoice_list.js"
}
Routing & website
website_route_rules = [
{"from_route": "/orders/<name>", "to_route": "order-detail"},
]
update_website_context = ["my_app.context.update_context"]
Fixtures
fixtures = [
{"dt": "Role", "filters": [["name", "in", ["My Custom Role"]]]},
{"dt": "Custom Field", "filters": [["dt", "in", ["Customer", "Supplier"]]]},
]
Whitelisted methods
override_whitelisted_methods = {
"frappe.client.get_count": "my_app.custom.get_count"
}
Boot session
boot_session = "my_app.boot.boot_session" # Add data to frappe.boot
For canonical updates and any newer hooks, see frappeframework.com/docs/v15/user/en/python-api/hooks.