hooks.py is the control panel of a Frappe app. It's a flat Python file of module-level variables that the framework reads to wire your app into the system. You use it to react to document events, schedule jobs, override classes, and inject assets. Every app has one at <app>/<app>/hooks.py.
Defining hooks in your app
Frappe imports hooks.py and reads its module-level variables, so a hook is just a top-level assignment. There is no registration function to call.
A hook value is one of three shapes:
- a string dotted path to a function, like
"library.install.after_install" - a list of dotted paths
- a dict that maps a key (often a doctype) to one or more dotted paths
# single function
after_install = "library.install.after_install"
# list of functions
before_request = ["library.startup.before_request"]
# dict keyed by doctype
doc_events = {
"Library Loan": {
"on_submit": "library.events.loan.on_submit",
},
}
Hooks are additive across apps: when multiple installed apps define the same hook, Frappe merges them. So you never edit another app's hooks.py to extend it. Define the same hook in your own app and Frappe combines the values. Read the merged result for any hook with frappe.get_hooks("hook_name").
After changing hooks.py, run bench migrate (or at least bench clear-cache) so the new configuration is picked up.
App metadata
The top of the file identifies the app:
app_name = "library"
app_title = "Library"
app_publisher = "Acme Inc."
app_description = "Manage books and loans"
app_license = "MIT"
Document events
doc_events is the most-used hook. It binds functions to lifecycle events of a doctype, exactly like controller methods, but without modifying that doctype's controller. This is how you react to another app's doctypes.
doc_events = {
"Library Loan": {
"on_submit": "library.events.loan.on_submit",
"on_cancel": "library.events.loan.on_cancel",
},
# apply to every doctype with "*"
"*": {
"validate": "library.events.audit.log_change",
},
}
The handler receives the document and the event name:
# library/events/loan.py
def on_submit(doc, method):
frappe.msgprint(f"Loan {doc.name} issued")
Supported events mirror the lifecycle: before_insert, after_insert, before_validate, validate, before_save, on_update, before_submit, on_submit, before_cancel, on_cancel, before_update_after_submit, on_update_after_submit, before_rename, after_rename, on_trash, after_delete, and on_change.
Scheduler events
scheduler_events registers functions to run on a schedule (the scheduler must be enabled for the site). Use the named frequencies or a cron expression:
scheduler_events = {
"daily": [
"library.tasks.send_due_reminders",
],
"hourly": [
"library.tasks.sync_catalog",
],
"cron": {
"0/15 * * * *": [
"library.tasks.poll_returns",
],
},
}
Available frequencies include all (every scheduler tick), hourly, daily, weekly, monthly, their _long variants (run on the long-running worker), the _maintenance variants, and cron for arbitrary expressions. See Background Jobs for how these execute.
Overriding classes and methods
override_doctype_class
This is an advanced hook. Reach for doc_events first, and only override the class when you need to change methods those hooks can't reach.
Replace a DocType's controller class with your own subclass. Always extend the base controller class and call super() so the original behavior still runs. This is useful for changing behavior of a DocType you don't own:
override_doctype_class = {
"ToDo": "library.overrides.todo.CustomToDo",
}
# library/overrides/todo.py
from frappe.desk.doctype.todo.todo import ToDo
class CustomToDo(ToDo):
def validate(self):
super().validate()
# extra logic
override_whitelisted_methods
Redirect calls to a whitelisted method to a replacement of yours:
override_whitelisted_methods = {
"frappe.client.get_count": "library.overrides.get_count",
}
See Whitelisted Methods.
Permission hooks
These extend access control. See Permissions in code for how they're applied.
# add SQL conditions to list queries (row-level filtering)
permission_query_conditions = {
"Library Loan": "library.permissions.loan_query_conditions",
}
# custom per-document permission check
has_permission = {
"Library Loan": "library.permissions.has_loan_permission",
}
# library/permissions.py
def loan_query_conditions(user=None, doctype=None):
user = user or frappe.session.user
return f"`tabLibrary Loan`.owner = {frappe.db.escape(user)}"
def has_loan_permission(doc, ptype, user, debug=False):
return doc.owner == user
Request and job hooks
Run code at the boundaries of every request and background job:
before_request = ["library.startup.before_request"]
after_request = ["library.startup.after_request"]
before_job = ["library.startup.before_job"]
after_job = ["library.startup.after_job"]
Login and session hooks
on_session_creation = ["library.auth.on_session_creation"]
on_login = ["library.auth.on_login"]
on_logout = ["library.auth.on_logout"]
# add data to the JS bootinfo sent to the desk on load
extend_bootinfo = ["library.boot.boot_session"]
on_login, on_session_creation, and on_logout are each called with the login_manager. extend_bootinfo is called with the bootinfo dict, which you mutate in place; the result is available client-side as frappe.boot.
# library/boot.py
def boot_session(bootinfo):
bootinfo.my_global_key = "my_global_value"
Authentication hooks
auth_hooks run during request authentication, before the request is handled. Use them to read a custom header or token, verify it, and map the request to a user with frappe.set_user(). They take no arguments.
auth_hooks = ["library.auth.validate_custom_jwt"]
Do not raise if verification fails. Return without setting a user and the request stays a Guest request, so other auth hooks still get a chance to run.
Assets
Inject JavaScript and CSS into the Desk, the website, or specific DocType forms:
app_include_js = ["library.bundle.js"] # desk
app_include_css = ["library.bundle.css"]
web_include_js = ["library_web.js"] # website pages
web_include_css = ["library_web.css"]
doctype_js = {"Library Loan": "public/js/library_loan.js"} # form view
page_js = {"background_jobs": "public/js/custom_background_jobs.js"} # desk page
webform_include_js = {"ToDo": "public/js/custom_todo.js"} # standard web form
Jinja and queries
# expose methods/filters to Jinja templates and print formats
jinja = {
"methods": ["library.utils.jinja_methods"],
"filters": ["library.utils.jinja_filters"],
}
# replace the default link-field search query for a doctype
standard_queries = {"Library Book": "library.queries.book_query"}
Website hooks
These hooks control how portal (website) pages render and route.
Context
When a portal page renders, Frappe builds a context dict of values the template can use. website_context is a flat dict of static overrides. update_website_context points to a function for dynamic changes; it gets the context dict and can mutate it or return a dict to merge.
website_context = {"favicon": "/assets/library/img/favicon.png"}
update_website_context = "library.website.update_context"
# library/website.py
def update_context(context):
context.my_key = "my_value"
Redirects and route rules
website_redirects maps source routes to targets. The source can be a plain path or a regex, and the target can reference capture groups.
website_redirects = [
{"source": "/compare", "target": "/comparison"},
{"source": r"/docs(/.*)?", "target": r"https://docs.example.com/\1"},
]
website_route_rules maps a URL pattern to a controller path. Use it for clean, dynamic URLs.
website_route_rules = [
{"from_route": "/projects/<name>", "to_route": "library/projects/project"},
]
The controller reads the matched parameter from frappe.form_dict:
# library/projects/project.py
def get_context(context):
context.project = frappe.get_doc("Project", frappe.form_dict.name)
Home page
The root URL (/) renders www/index by default. Override it, top to bottom in increasing priority:
# static override
home_page = "homepage"
# per-role override
role_home_page = {"Customer": "orders", "Supplier": "bills"}
# full control: function receives the user, returns a route
get_website_user_home_page = "library.website.get_home_page"
If more than one is set, get_website_user_home_page wins over role_home_page, which wins over home_page.
Portal sidebar
Some portal views show a sidebar of links. portal_menu_items are defined in code and fixed. standard_portal_menu_items sync to Portal Settings, where a System User can edit them later.
standard_portal_menu_items = [
{"title": "Orders", "route": "/orders", "role": "Customer"},
]
Clearing website cache
Frappe caches rendered web pages. website_clear_cache runs when that cache is cleared. The function gets a path: a route when one page is cleared, or None when all pages are cleared.
website_clear_cache = "library.website.clear_cache"
User data privacy
Frappe ships personal data download and deletion. user_data_fields declares which doctypes hold personal data so those flows can find and redact it. Each entry is a dict:
user_data_fields = [
{"doctype": "Access Log", "strict": True},
{"doctype": "Contact", "filter_by": "email_id", "rename": True},
{"doctype": "File", "filter_by": "attached_to_name", "redact_fields": ["file_name", "file_url"]},
{"doctype": "Email Unsubscribe", "filter_by": "email", "partial": True},
]
| Key | Meaning |
|---|---|
doctype |
The doctype that holds user data. |
filter_by |
Field used to find the user's records. Defaults to owner. |
redact_fields |
Fields to redact. If unset, redacts personal data from all text fields. |
partial |
Redact the user's name and username from all text fields. |
rename |
Rename the document to anonymize it when its name contains user data. |
strict |
Redact data from every record of the doctype, not just ones the user owns. |
Download only uses doctype and filter_by.
Fixtures
Fixtures let you ship database records as part of your app. They're useful for records you create through the Desk but want to version with code, like Custom Fields, Roles, or Workflows.
The fixtures hook lists the doctypes (and optional filters) to include:
fixtures = [
"Custom Field",
{"dt": "Role", "filters": [["name", "in", ["Librarian"]]]},
]
An entry is either a doctype name (export every record) or a dict with dt and filters (or or_filters) to export a subset.
Export the records to JSON with:
bench --site <site> export-fixtures --app library
This writes one file per doctype to <app>/<app>/fixtures/, for example fixtures/custom_field.json. Commit those files.
On bench migrate, Frappe imports every JSON file under fixtures/ for each installed app, overwriting matching records on the target site. So fixtures sync one way: export from where you author them, then migrate everywhere else to apply them. If a fixture's doctype doesn't exist on the site, that file is skipped.
Install and migrate hooks
before_install = "library.install.before_install"
after_install = "library.install.after_install"
before_migrate = "library.migrate.before_migrate"
after_migrate = "library.migrate.after_migrate"
before_uninstall = "library.install.before_uninstall"
after_uninstall = "library.install.after_uninstall"
before_uninstall/after_uninstall run when the app is removed from a site with bench uninstall-app.
after_sync runs after the app's fixtures are synced. before_tests runs once before the test suite starts, which is where you seed data your tests depend on.
after_sync = "library.install.after_sync"
before_tests = "library.tests.before_tests"
Other app install hooks
You can also react to installation and uninstallation of other apps on a site, useful when toggling functionality when a certain app (e.g. integrations) is installed.
before_app_install = "library.integrations.before_app_install"
after_app_install = "library.integrations.after_app_install"
before_app_uninstall = "library.integrations.before_app_uninstall"
after_app_uninstall = "library.integrations.after_app_uninstall"
File hooks
These override how user-uploaded files are stored, so you can write to a CDN or object store instead of the local disk.
before_write_file = "library.overrides.file.before_write"
write_file = "library.overrides.file.write_file"
delete_file_data_content = "library.overrides.file.delete_file"
before_write_file runs before a file is saved. write_file replaces the save itself. delete_file_data_content replaces deletion.
Discovering hooks
The framework's own hooks.py is the most complete reference for what each hook expects, so read frappe/hooks.py in the source. To see the merged value across all installed apps for any hook:
frappe.get_hooks("doc_events")
frappe.get_hooks("scheduler_events")
Hook index
The commonly used hooks, alphabetically, with the section that covers each.
| Hook | Section |
|---|---|
after_app_install |
Other app install hooks |
after_app_uninstall |
Other app install hooks |
after_install |
Install and migrate hooks |
after_job |
Request and job hooks |
after_migrate |
Install and migrate hooks |
after_request |
Request and job hooks |
after_sync |
Install and migrate hooks |
after_uninstall |
Install and migrate hooks |
app_include_css |
Assets |
app_include_js |
Assets |
app_name, app_title, ... |
App metadata |
auth_hooks |
Authentication hooks |
before_app_install |
Other app install hooks |
before_app_uninstall |
Other app install hooks |
before_install |
Install and migrate hooks |
before_job |
Request and job hooks |
before_migrate |
Install and migrate hooks |
before_request |
Request and job hooks |
before_tests |
Install and migrate hooks |
before_uninstall |
Install and migrate hooks |
before_write_file |
File hooks |
delete_file_data_content |
File hooks |
doc_events |
Document events |
doctype_js |
Assets |
extend_bootinfo |
Login and session hooks |
fixtures |
Fixtures |
get_website_user_home_page |
Website hooks |
has_permission |
Permission hooks |
home_page |
Website hooks |
jinja |
Jinja and queries |
on_login |
Login and session hooks |
on_logout |
Login and session hooks |
on_session_creation |
Login and session hooks |
override_doctype_class |
override_doctype_class |
override_whitelisted_methods |
override_whitelisted_methods |
page_js |
Assets |
permission_query_conditions |
Permission hooks |
portal_menu_items |
Website hooks |
role_home_page |
Website hooks |
scheduler_events |
Scheduler events |
standard_portal_menu_items |
Website hooks |
standard_queries |
Jinja and queries |
update_website_context |
Website hooks |
user_data_fields |
User data privacy |
web_include_css |
Assets |
web_include_js |
Assets |
webform_include_js |
Assets |
website_clear_cache |
Website hooks |
website_context |
Website hooks |
website_redirects |
Website hooks |
website_route_rules |
Website hooks |
write_file |
File hooks |
See also
- Controllers & Lifecycle: the events
doc_eventshooks into. - Permissions in code:
permission_query_conditionsandhas_permission. - Whitelisted Methods: overriding endpoints.
- Background Jobs: how scheduler events run.