# hooks.py (ERPNext glossary)

Canonical: https://erpfly.com/glossary/hooks-py/
Last updated: September 14, 2026

**Definition:** hooks.py is the configuration file inside every Frappe app's Python package where the app declares how it plugs into the framework, such as document event handlers, scheduled jobs, fixtures, form script includes and class overrides. Frappe merges the hooks of all apps installed on a site and applies them together.

Also called: Frappe hooks, app hooks, hooks file, doc_events

Adding a credit check to Sales Order shouldn't mean editing ERPNext's own controller, because that edit blocks your next update. What you do instead is tell Frappe to call a function of yours whenever a Sales Order is validated. That instruction goes in `hooks.py`, which `bench new-app` creates at `apps/<app>/<app>/hooks.py` with most of the options stubbed out as comments.

### How Frappe reads the file

A hook is plain Python: a module-level variable with a known name, holding a string, list or dict. Frappe doesn't run your hooks in isolation. It loads the file from every app installed on the site and merges values that share a name into lists, which is why ERPNext's `doc_events` and yours can both fire on the same save.

Some hooks use every collected value, like included JS files or document events. Others can only have one winner, like `override_whitelisted_methods`. For those, the app installed last on the site wins. The order can be changed from the Installed Applications page if you really need to.

Outside developer mode, the merged hooks are cached. Editing the file on a production bench does nothing visible until the cache is cleared and the processes restart. `bench migrate` clears the cache, and the docs say scheduler changes specifically need `migrate` before they take effect.

### A realistic example

```python
# acme_custom/hooks.py
app_name = "acme_custom"
app_title = "Acme Custom"

required_apps = ["erpnext"]

doc_events = {
    "Sales Order": {
        "validate": "acme_custom.sales_order.check_credit_hold",
        "on_submit": "acme_custom.sales_order.notify_warehouse",
    },
}

scheduler_events = {
    "daily": ["acme_custom.tasks.close_stale_quotations"],
    "cron": {
        "0 7 * * 1": ["acme_custom.tasks.send_weekly_ar_summary"],
    },
}

doctype_js = {"Sales Order": "public/js/sales_order.js"}

fixtures = [
    {"dt": "Custom Field", "filters": [["module", "=", "Acme Custom"]]},
]
```

```python
# acme_custom/sales_order.py
import frappe


def check_credit_hold(doc, method=None):
    if frappe.db.get_value("Customer", doc.customer, "custom_credit_hold"):
        frappe.throw(f"{doc.customer} is on credit hold. Ask accounts to release it.")


def notify_warehouse(doc, method=None):
    ...
```

Every `doc_events` handler receives the document and the event name. The event keys match controller method names, so `validate`, `on_update`, `on_submit` and `on_cancel` all work, and `"*"` in place of a DocType name applies a handler to every DocType. This is the pattern erpfly generates when a requirement touches a standard ERPNext document: the logic sits in your app, wired through `hooks.py`, and ERPNext's files stay untouched.

### Hooks you'll reach for first

- `doc_events` for reacting to saves, submits and cancels on DocTypes you don't own.
- `scheduler_events` with `hourly`, `daily`, `weekly`, `monthly`, their `_long` variants for slow jobs, and `cron` for exact times.
- `fixtures` to ship Custom Fields and other records with the app.
- `doctype_js` to extend a standard form's client script.
- `override_doctype_class` to swap a DocType's controller class. On v16, the docs recommend `extend_doctype_class` when you only need to add behaviour, because several apps can extend the same class without fighting.
- `after_install` and `after_migrate` for setup code.

### When a hook doesn't fire

Check the dotted path first. A typo in `acme_custom.sales_order.check_credit_hold` fails at runtime, not when the file loads. Then check that the app is actually installed on that site with `bench --site <site> list-apps`, since installing on the bench alone isn't enough. After that, clear the cache and restart.

If you're weighing a hook in a custom Frappe app against a Server Script for the same rule, we lay out where each one fits in [Server Scripts vs a custom app](https://erpfly.com/blog/server-scripts-vs-custom-app-erpnext/). For larger builds, see [ERPNext custom module development](https://erpfly.com/erpnext-custom-module-development/).

See also: https://erpfly.com/glossary/frappe-app/, https://erpfly.com/glossary/fixtures/, https://erpfly.com/glossary/server-script/, https://erpfly.com/glossary/bench/

### Sources

- [Hooks, Frappe Framework documentation](https://docs.frappe.io/framework/user/en/python-api/hooks)
- [Controllers, Frappe Framework documentation](https://docs.frappe.io/framework/user/en/basics/doctypes/controllers)
- [frappe/__init__.py, get_hooks (version-15), frappe/frappe on GitHub](https://github.com/frappe/frappe/blob/version-15/frappe/__init__.py)