Skip to main content

Chapter 3 of 7 · What runs when

hooks.py: changing Frappe without editing it

What an app can set, how apps combine, and what a hook cannot do.

Reviewed 14 Oct 20262 min readFrappe v16.52.0

hooks.py is a file in every Frappe app that tells the framework what the app wants. Frappe reads it from every installed app and merges the results. That merge is the whole extension system. You never edit Frappe. You add a line that says "also run this".

Say what you want to do, and the module shows the hook, a short example, when it fires and what it cannot do.

Try it

What do you want to do?

Pick a goal. The answer is the hooks.py key that does it, and what it cannot do.

I want to

The hook
doc_events
When it fires
On that event of that DocType, right after the controller's own method. Use "*" for every DocType.
What it needs
Clear the cache (bench clear-cache). The handler is def on_invoice_submit(doc, method=None).
What it cannot do
Commit on v16: a commit inside a handler is skipped, with a warning.

Example, not from a client

Frappe v16.52.0. Where v15.123.0 differs, the answer says so.

How do hooks from several apps combine?

Lists are joined in the order of the site's installed-apps list. Dictionaries such as doc_events are merged key by key, and a key can be a tuple of DocTypes. So two apps can both hook Sales Invoice on on_submit, and both run, in that order. Outside developer mode the merged hooks are cached, so a change to hooks.py is not live until you clear the cache with bench clear-cache.

Which hook does what?

doc_events runs your function on a document event of any DocType, yours or another app's. * means every DocType. ERPNext uses it itself: submitting or cancelling a Stock Entry updates the Material Request.

scheduler_events lists functions to run hourly, daily or on a cron line. They become rows in Scheduled Job Type only when bench migrate runs (chapter 5).

permission_query_conditions returns SQL that narrows a DocType's list. has_permission can deny one document. Neither can grant (chapter 4).

extend_doctype_class, on v16, adds a mixin to a DocType's controller. override_doctype_class replaces the controller (chapter 7).

fixtures exports records such as Custom Fields to JSON in your app and imports them at migrate. before_migrate and after_migrate run around the migrate.

naming_series_variables adds a token such as FY to naming series (chapter 6). before_job and after_job wrap every background job. override_whitelisted_methods sends an API call to your function instead of Frappe's; if two apps override one method, the last one wins.

What can a hook not do?

It cannot commit. On v16 a frappe.db.commit() inside a doc_events handler does nothing and warns. has_permission can only deny, and permission_query_conditions shapes get_list, which the list view and reports use, but not get_all, which skips permissions. Overriding one DocType's class from two apps does not stack: the last app wins.

How do you see what Frappe actually merged?

In bench console, frappe.get_hooks("doc_events") returns the merged result across apps. When a handler does not run, look there before anywhere else.

Where this comes from

Read at Frappe v16.52.0 and ERPNext v16.50.0. “(v15)” marks Frappe v15.123.0 and “(ERPNext v15)” marks ERPNext v15.122.0. Each link opens that file at that tag, on that line.

Official page: Hooks, read 14 October 2026. Blog: building a custom Frappe app that survives upgrades.

Your next step

See it live

Run a sprint

Finish a task in this week's sprint, log the hour it took, and see the sprint's burndown, all on ERPNext's own records.

You end with hours booked on a task · 2 screens · about 3 min

No sign-up, no card, no account.

Next step · Architecture guide

Now see how we build a Frappe app

A separate app in your own Git, so upgrades keep working.