---
title: "Hooks"
space: "Framework"
url: "https://docs.frappe.io/framework/user/en/python-api/hooks"
updated: "2026-07-23"
---

Hooks allow you to "hook" into functionality and events of core parts of the Frappe Framework. This page documents all of the hooks provided by the framework.

> Jump to list of all [available hooks](#list-of-available-hooks) in Frappe.

## How does hooks work?

Hooks are places in the core code that allow an app to override the standard implementation or extend it. Hooks are defined in **hooks.py** of your app.

Let's learn by example. Add the following hooks in your app's **hooks.py**.

```
test_string = "value"
test_list = ["value"]
test_dict = {
    "key": "value"
}
  

```

Now, open the python console by running the command `bench --site sitename console` and run the following lines:

```
❯ bench --site sitename console
Apps in this namespace:
frappe, frappe_docs

In [1]: frappe.get_hooks("test_string")
Out[1]: ["value"]

In [2]: frappe.get_hooks("test_dict")
Out[2]: {"key": ["value"]}

In [3]: frappe.get_hooks("test_list")
Out[3]: ["value"]
  

```

When you call `frappe.get_hooks`, it will convert all the values in a list. This means that if the hook is defined in multiple apps, the values will be collected from those apps. This is what enables the cascading nature of hooks.

Now, the hook value can be consumed in different ways. For example, for including JS assets using `app_include_js`, all of the values are included. But for overriding whitelisted method, the last value in the list is used.

So the implementation of the hook is totally dependent on how the author of the feature intended it to be used.

## How are conflicting hooks resolved?

Hooks are resolved using "last writer wins" strategy. Last installed app on site will have highest priority over others.

- When the hook overrides existing behaviour like overriding a class then only overrides from last app will work.
- When the hook extends behaviour then extensions will be applied in order of installation on the site.

> If you need to change this order you can do so by going to "Installed Applications" page and clicking on "Update Hooks Resolution Order"

## App Meta Data

These are automatically generated when you create a new app. Most of the time you don't need to change anything here.

1. `app_name` - slugified name of the app
2. `app_title` - presentable app name
3. `app_publisher`
4. `app_description`
5. `app_version`
6. `app_icon`
7. `app_color`

## Javascript / CSS Assets

The following hooks allow you to inject static JS and CSS assets in various parts of your site.

### Desk

These hooks allow you to inject JS / CSS in `desk.html` which renders the [Desk](/framework/v14/user/en/desk).

```
# injected in desk.html
app_include_js = "assets/js/app.min.js"
app_include_css = "assets/js/app.min.css"

# All of the above support a list of paths too
app_include_js = ["assets/js/app1.min.js", "assets/js/app2.min.js"]
  

```

### Portal

These hooks allow you to inject JS / CSS in `web.html` which renders the [Portal](/framework/v14/user/en/portal-pages).

```
# injected in the web.html
web_include_js = "assets/js/app-web.min.js"
web_include_css = "assets/js/app-web.min.css"
# All of the above support a list of paths too
web_include_js = ["assets/js/web1.min.js", "assets/js/web2.min.js"]
  

```

### Web Form

These hooks allow you to add inject static JS and CSS assets in `web_form.html` which is used to render Web Forms. These will work only for Standard Web Forms.

```
webform_include_js = {"ToDo": "public/js/custom_todo.js"}
webform_include_css = {"ToDo": "public/css/custom_todo.css"}
  

```

> For user created Web Forms, you can directly write the script in the form > itself.

### Page

These hooks allow you to inject JS assets in Standard Desk Pages.

```
page_js = {"page_name" : "public/js/file.js"}
  

```

For e.g., Background Jobs is a standard page that is part of Core module in Frappe Framework. To add custom behaviour in that page you can add a JS file in your custom app `custom_app/public/js/custom_background_jobs.js` and add the following line in your hooks file.

**custom_app/hooks.py**

```
page_js = {"background_jobs": "public/js/custom_background_jobs.js"}
  

```

## Sounds

Frappe ships with a set of audio notifications for events like a success action, document submission, error, etc. You can add your own sounds using the `sounds` hook.

**app/hooks.py**

```
sounds = [
    {"name": "ping", "src": "/assets/app/sounds/ping.mp3", "volume": 0.2}
]
  

```

You can play your added sound using the client utility method:

```
frappe.utils.play_sound("ping")
  

```

## Install Hooks

These hooks allow you to run code before and after installation of your app. For example, [ERPNext](https://github.com/frappe/erpnext) has these [defined](https://github.com/frappe/erpnext/blob/ecfa6344c02b18455637455742202c7fadf1b35d/erpnext/hooks.py#L42-L43).

```
# python module path
before_install = "app.setup.install.before_install"
after_install = "app.setup.install.after_install"
after_sync = "app.setup.install.after_sync"
  

```

**app/setup/install.py**

```
# will run before app is installed on site
def before_install():
    pass

# will run after app is installed on site
def after_install():
    pass

# will run after app fixtures are synced
def after_sync():
    pass
  

```

## Uninstall Hooks

These hooks allow you to run code before and after uninstallation of your app.

**app/hooks.py**

```
before_uninstall = "app.setup.uninstall.before_uninstall"
after_uninstall = "app.setup.uninstall.after_uninstall"
  

```

**app/setup/uninstall.py**

```
# will run before app is uninstalled from site
def before_uninstall():
    pass

# will run after app is uninstalled from site
def after_uninstall():
    pass
  

```

## Migrate Hooks

These hooks allow you to run code before and after a migration is run on your site via the command `bench --site sitename migrate`.

**app/hooks.py**

```
before_migrate = "app.migrate.before_migrate"
after_migrate = "app.migrate.after_migrate"
  

```

**app/migrate.py**

```
def after_migrate():
    # run code after site migration
    pass
  

```

## Build Hook

This hook allows you to extend the build system via the `bench build` command. It runs after completing builds for all the apps on the bench.

**app/hooks.py**

```
after_build = "studio.build.after_build"
```

**studio/build.py**

```python
def after_build() -> None:
	# run code after `bench build`
	pass
```

## Test Hooks

This hook allows you to run code before tests are run on a site. You can use this hook to add seed data to your database which will be available to your tests.

**app/hooks.py**

```
before_tests = "app.tests.before_tests"
  

```

**app/migrate.py**

```
def before_tests():
    # add seed data to the database
    pass
  

```

## File Hooks

These hooks allows you to change the implementation of handling user uploaded files.

**app/hooks.py**

```
before_write_file = "app.overrides.file.before_write"
write_file = "app.overrides.file.write_file"
delete_file_data_content = "app.overrides.file.delete_file"
  

```

**app/overrides/file.py**

```
# will run before file is written to disk
def before_write():
    pass

# will override the implementation of writing file to disk
# can be used to upload files to a CDN instead of writing
# the file to disk
def write_file():
    pass


# will override the implementation of deleting file from disk
# can be used to delete uploaded files from a CDN instead of
# deleting file from disk
def delete_file():
    pass
  

```

## Email Hooks

These hooks allows you to change the default email module implementation of sending emails and setting default sender address.

**app/hooks.py**

```
override_email_send = "app.overrides.email.send"
get_sender_details = "app.overrides.email.get_sender_details"
  

```

By default frappe uses the currently logged in users name and id as sender details on all emails. This can be overriden with `get_sender_details` hook. And if you want to extend the email modules functionality by using a thirdy party server or app for sending emails then you can use `override_email_send` hook. This hook will send all the email information (sender, recipient, content(mime)) to a function in custom_app.

**app/overrides/email.py**

```
# will be edited as "John Doe <johndoe@example.com>"
def get_sender_details():
    return "John Doe", "johndoe@example.com"

# self - EmailQueue object refrence for updating status
def send(self, sender, recipient, msg):
    # smtp or http request
    self.update_status("Sending")
  

```

> Note: You'll have to handle the status change of email queue in your custom app depending on the webhook response you get from your mail provider/server

## Extend Bootinfo

After a successful login, the Desk is injected with a dictionary of global values called `bootinfo`. The `bootinfo` is available as a global object in Javascript as `frappe.boot`.

The `bootinfo` dict contains a lot of values including:

- System defaults
- Notification status
- Permissions
- User settings
- Language and timezone info

You can add global values that makes sense for your app via the `extend_bootinfo` hook.

```
# python module path
extend_bootinfo = "app.boot.boot_session"
  

```

The method is called with one argument `bootinfo`, on which you can directly add/update values.

**app/boot.py**

```
def boot_session(bootinfo):
 bootinfo.my_global_key = "my_global_value"
  

```

Now, you can access the value anywhere in your client side code.

```
console.log(frappe.boot.my_global_key)
  

```

## Website Context

When a Portal Page is rendered, a dictionary is built with all of the possible variables that the page might need to render. This dict is also known as `context`. You can use these hooks to add or modify values in this dict.

**app/hooks.py**

```
website_context = {
    "favicon": "/assets/app/image/favicon.png"
}
update_website_context = "app.overrides.website_context"
  

```

The `website_context` hook is a simple dict of key value pairs. Use this hook for simple value overrides.

You can use the `update_website_context` hook for more complex scenarios as it allows you to manipulate the context dict in a python method. The method is called with one argument, which is the `context` dict. You can either modify the context directly by mutating it or return a dict that will be merged with `context`.

**app/overrides.py**

```
def website_context(context):
 context.my_key = "my_value"
  

```

## Website Controller Context

Frappe ships with standard web pages like `/404` and `/about`. If you want to extend the controller context for these pages you can use the `extend_website_page_controller_context` hook.

**app/hooks.py**

```
extend_website_page_controller_context = {
    "frappe.www.404": "app.pages.context_404"
}
  

```

The above hook configuration will allow you to extend the context of the 404 page so that you can add your own keys or modify existing ones.

**app/pages.py**

```
def context_404(context):
    # context of the 404 page
    context.my_key = "my_value"
  

```

## Web pages with dynamic routes

Dynamic routes are the routes with dynamic values in them.

**Example:**

```
/profile/<name>
```

here "name" is the dynamic part but same profile page is rendered. By default, Frappe supports dynamic routes from documents in "Web Page" doctype. To add more dynamic routes, `get_web_pages_with_dynamic_routes` can be used.

**app/[hooks.py](http://hooks.py)**

```
get_web_pages_with_dynamic_routes = "script.get_web_pages_with_dynamic_routes"
  

```

**script.py**

```
def get_web_pages_with_dynamic_routes():
 return [{
       "doctype": "Custom Web Page", // Doctype extended from WebsiteGenerator https://frappeframework.com/docs/user/en/guides/portal-development/generators
       "route": "/profile/<name>",
       "name": "profile-page" // name of the web view document to render
    }, ...]
```

## Website Clear Cache

Frappe Framework caches a lot of static web pages for fast subsequent rendering. If you have created web pages that use cached values, and you want to invalidate the cache, this hook is place to do it.

**app/hooks.py**

```
website_clear_cache = "app.overrides.clear_website_cache"
  

```

The method is called with one argument `path`. `path` is set when cache is being cleared for one route, and is `None` when cache is cleared for all routes. You need to handle this case if your cache is page specific.

**app/overrides.py**

```
def clear_website_cache(path=None):
    if path:
        # clear page related cache
    else:
        # clear all cache
  

```

## Website Redirects

Website Redirects allow you to define redirects from one route to another. Frappe will generate a 304 Redirect response when the source URL is requested and redirect to the target URL. You can redirect plain URLs or you can use regex to match your URLs.

**app/hooks.py**

```
website_redirects = [
    {"source": "/compare", "target": "/comparison"},
    {"source": "/docs(/.*)?", "target": "https://docs.tennismart.com/\1"},
    {"source": r'/items/item\?item_name=(.*)', "target": '/items/\1', match_with_query_string=True},
]
  

```

The above configuration will result in following redirects:

- `/compare` to `/comparison`
- `/docs/getting-started` to `https://docs.tennismart.com/getting-started`
- `/docs/help` to `https://docs.tennismart.com/help`
- `/items/item?item_name=racket` to `https://docs.tennismart.com/items/racket`

## Website Route Rules

Website Route Rules allow you to map URLs to custom controllers. This is commonly used to generate clean URLs for pages.

Let's say you want to have `/projects` route to display list of projects. This can be done by creating a `projects.html` and `projects.py` in `www` folder.

You also want to have `/project/<name>` route to show a project page where name is the dynamic. To do this you can use the `website_route_rules` hook.

**app/hooks.py**

```
website_route_rules = [
    {"from_route": "/projects/<name>", "to_route": "app/projects/project"},
]
  

```

Now, you can create your controller files in `app/projects` folder.

**app/projects/project.py**

```
def get_context(context):
    project_name = frappe.form_dict.name
    project = frappe.get_doc("Project", project_name)
    context.project = project
  

```

**app/projects/project.html**

```
<h1>{{ project.title }}</h1>
<p>{{ project.description }}</p>
  

```

## Website Path Resolver

Frappe does some standard route resolving eg. any request to "/profile" gets internally converted to "/me". `website_path_resolver` can be used to override this behaviour.

**app/[hooks.py](http://hooks.py)**

```
website_path_resolver = "path.to.custom_resolver_method"
```

**Note:** Yourcustom_resolver_method will get the requested route, is expected to return a processed route.

## Website 404

Frappe renders a default `/404` route when a page is not found. You can change this using the `website_catch_all` hook.

**app/hooks.py**

```
website_catch_all = "not_found"
  

```

The above configuration will render `/not_found` when a 404 is occurred. It is upto you to implement the template `www/not_found.html` and controller `www/not_found.py`.

## Default Homepage

Homepage is the page which is rendered when you visit the root URL (`/`) of your site. There are multiple ways to configure what page is rendered as the default homepage.

By default, the homepage is `index`. So, frappe will try to render `index.html` from `www` folder. This can be overridden using the `homepage` hook.

**app/hooks.py**

```
homepage = "homepage"
  

```

The above configuration will load the `www/homepage.html` as the default homepage.

You can also have role based homepage by using the `role_home_page` hook.

**app/hooks.py**

```
role_home_page = {
    "Customer": "orders",
    "Supplier": "bills"
}
  

```

The above configuration will make `/orders` the default homepage for users with the **Customer** role and `/bills` for users with the **Supplier** role.

You can have even more control over the logic by using the `get_website_user_home_page` hook.

**app/hooks.py**

```
get_website_user_home_page = "app.website.get_home_page"
  

```

**app/website.py**

```
def get_home_page(user):
    if is_projects_user(user):
        return "projects"
    if is_partner(user):
        return "partner-dashboard"
    return "index"
  

```

> If all of these hooks are defined, the `get_website_user_home_page` will have > higher priority over the others, and `role_home_page` will have higher > priority over `homepage`.

## Portal Sidebar

Some Portal views are shown with a sidebar with links to quickly jump to pages. These sidebar items can be customized via hooks.

**app/hooks.py**

```
portal_menu_items = [
    {"title": "Dashboard", "route": "/dashboard", "role": "Customer"},
    {"title": "Orders", "route": "/orders", "role": "Customer"},
]
  

```

The above configuration will add two sidebar links for users with the role Customer.

![Portal Sidebar](/files/hooks-portal-menu-items.png)



These sidebar items are hardcoded in your app so they are not customizable from Desk. For e.g., if you want to hide a sidebar link temporarily you will have to make changes in your code.

There is another hook called `standard_portal_menu_items` which allows you to do that. The sidebar links set in `standard_portal_menu_items` hook will be synced with the database.

**app/hooks.py**

```
standard_portal_menu_items = [
    {"title": "Dashboard", "route": "/dashboard", "role": "Website Manager"},
    {"title": "Orders", "route": "/orders", "role": "Website Manager"},
]
  

```

The above configuration will sync sidebar items to the Portal Settings which can later be edited by any System User.

![Portal Settings](/files/hooks-standard-portal-menu-items.png)



## Brand HTML

This hook allows you to customize the brand logo in the navbar of your website.

**app/hooks.py**

```
brand_html = '<div><img src="tennismart.png"> TennisMart</div>'
  

```

If the `brand_html` is defined, it will override the default brand html in the navbar. It is not recommended to use hooks to change your brand logo, unless you want to version control it, otherwise you can use Website Settings to change it.

## Base Template

When a web page is rendered, it extends `templates/base.html` by default. You can override the base template by overriding the `base_template` hook.

**app/hooks.py**

```
base_template = "app/templates/my_custom_base.html"
  

```

You can also customize base templates based on routes. For e.g., if you want to use a different base template for all the routes that start with `docs/*` then you can use the `base_template_map` hook. The key must be a regex that matches the route. All other routes will fallback to the default base template.

**app/hooks.py**

```
base_template_map = {
    r"docs.*": "app/templates/doc_template.html"
}
  

```

## Integrations

These hooks allow you to customize behaviour of 3rd-party integrations in Frappe.

### Braintree Success Page

This hook allows you to override the default redirect URL on successful payment of Braintree transaction.

**app/hooks.py**

```
braintree_success_page = "app.integrations.braintree_success_page"
  

```

The method is called with one argument `data` which has the meta data of the payment.

**app/integrations.py**

```
def braintree_success_page(data):
    # data.reference_doctype
    # data.reference_docname
    return "/thank-you"
  

```

## Calendars

The calendar hook is a list of doctype names which are shown as menu items for quick navigation from the Calendar page in Desk.

**app/hooks.py**

```
calendars = ["Appointment"]
  

```

![Event Menu Shortcuts](/files/hooks-event-menu-shortcuts.png)



## Clear Cache

This hook allows you to clear your app specific cache values when the global cache is being cleared by frappe.

**app/hooks.py**

```
clear_cache = "app.cache.clear_cache"
  

```

You can use this hook to clear your app specific cache. The method is called without any arguments.

**app/cache.py**

```
def clear_cache():
    frappe.cache().hdel("app_specific_cache")
  

```

## Default Mail Footer

If you want to set the default footer of all the emails that are sent out by Frappe, you can use the `default_mail_footer` hook.

**app/hooks.py**

```
default_mail_footer = """
 <div>
 Sent via <a href="https://tennismart.com" target="_blank">TennisMart</a>
</div>
"""
  

```

Now, all the emails will have **Sent via TennisMart** in the footer.

## Session Hooks

These hooks are triggered over the login lifecycle of a user. `on_login` is triggered immediately after a successful login, `on_session_creation` is triggered after the session is setup, `on_logout` is triggered after the user logs out.

**app/hooks.py**

```
on_login = "app.overrides.successful_login"
on_session_creation = "app.overrides.allocate_free_credits"
on_logout = "app.overrides.clear_user_cache"
  

```

The method will be called with one argument `login_manager`.

**app/overrides.py**

```
def allocate_free_credits(login_manager):
    # allocate free credits to frappe.session.user
    pass
  

```

## Auth Hooks

These hooks are triggered during request authentication. Custom headers, Authorization headers can be validated here, user is verified and mapped to the request using `frappe.set_user()`. Use `frappe.request` and `frappe.*` to validate request and map user.

**app/hooks.py**

```
auth_hooks = ["app.overrides.validate_custom_jwt"]
  

```

The method will be called during request authentication.

**app/overrides.py**

```
def validate_custom_jwt():
    # validate jwt from header, verify signature, set user from jwt.
    pass
  

```

Use this method to check for incoming request header, verify the header and map the user to the request. If header verification fails DO NOT throw error to continue with other hooks. Unverified request is treated as "Guest" request by default. You may use third party server, shared database or any alternative of choice to verify and map request and user.

## Fixtures

Fixtures are database records that are synced using JSON files when you install and update your site.

Let's say you want to create a set of categories in the database whenever you install your app. To do that, create the set of categories in your local site, and add the doctype name in the `fixtures` hook.

```
fixtures = [
    # export all records from the Category table
    "Category"
]
  

```

Now, run the following command:

```
bench --site sitename export-fixtures
  

```

This command will create a JSON file for each doctype which will contain the data to generate list of records. You can test this by creating a new site and by installing your app on that site.

You can also add conditions for exporting records.

```
fixtures = [
    # export all records from the Category table
    "Category",
    # export only those records that match the filters from the Role table
    {"dt": "Role", "filters": [["role_name", "like", "Admin%"]]},
]
  

```

Some fields are for internal use only. They will be set and kept up-to-date by the system automatically. These will not get exported: `modified_by`, `creation`, `owner`, `idx`, `lft` and `rgt`. For child table records, the following fields will not get exported: `docstatus`, `doctype`, `modified` and `name`.

## Document Hooks

### Modify List Query

You can customize how list of records are queried for a DocType by adding custom match conditions using the `permission_query_conditions` hook. This match condition must be a valid WHERE clause fragment for an SQL query.

**app/hooks.py**

```
permission_query_conditions = {
    "ToDo": "app.permissions.todo_query",
}
  

```

The method is called with a single argument `user` which can be `None`. The method should return a string that is a valid SQL WHERE clause.

**app/permissions.py**

```
def todo_query(user):
    if not user:
        user = frappe.session.user
    # todos that belong to user or assigned by user
    return "(`tabToDo`.owner = {user} or `tabToDo`.assigned_by = {user})".format(user=frappe.db.escape(user))
  

```

Now, if you use the `frappe.db.get_list` method, your WHERE clause will be appended to the query.

```
todos = frappe.db.get_list("ToDo", debug=1)

# output
'''
select `tabToDo`.`name`
from `tabToDo`
where ((`tabToDo`.owner = 'john@doe.com' or `tabToDo`.assigned_by = 'john@doe.com'))
order by `tabToDo`.`modified` DESC
'''
  

```

> This hook will only affect the result of `frappe.db.get_list` method and not > `frappe.db.get_all`.

### Document Permissions

You can modify the behaviour of `doc.has_permission` document method for any DocType and add custom permission checking logic using the `has_permission` hook.

**app/hooks.py**

```
has_permission = {
    "Event": "app.permissions.event_has_permission",
}
  

```

The method will be passed the `doc`, `user` and `permission_type` as arguments. It should return `True` or a `False` value. If `None` is returned, it will fallback to default behaviour.

**app/permissions.py**

```
def event_has_permission(doc, user=None, permission_type=None):
    # when reading a document allow if event is Public
    if permission_type == "read" and doc.event_type == "Public":
        return True

    # when writing a document allow if event owned by user
    if permission_type == "write" and doc.owner == user:
        return True

    return False
  

```

### Extend DocType Class

> Note: this is available in v16+

You can extend the class for standard doctypes by using the `extend_doctype_class` hook. This hook allows you to add properties, methods, and functionality to existing DocType classes without overriding them completely, enabling multiple apps to extend the same DocType class.

**app/hooks.py**

```python
extend_doctype_class = {
    "Address": ["app.extensions.address.AddressMixin"]
}
```

**app/extensions/address.py**

```python
from frappe.model.document import Document

class AddressMixin(Document):
    @property
    def full_address(self):
        return f"{self.address_line1}, {self.city}, {self.country}"
    
    def custom_validation(self):
        # Custom validation logic
        pass

    def validate(self):
        super.validate()
        self.custom_validation()
```

#### Multiple Extensions

You can define multiple extensions for the same DocType. For example, the `ValidationMixin` could be used in **Contact** as well as in **Address**, while the `GeocodingMixin` is used only in **Address**.

**app/hooks.py**

```python
extend_doctype_class = {
    "Address": [
        "app.extensions.address.GeocodingMixin",
        "app.extensions.common.ValidationMixin"
    ],
    "Contact": [
        "app.extensions.common.ValidationMixin"
    ]
}
```

#### Hooks Resolution Order

When multiple apps define extensions for the same DocType, the extensions are applied in the hooks resolution order. If apps are resolved in the order `frappe`, `app1`, `app2`, the final class will be:

```python
class ExtendedAddress(App2Mixin, App1Mixin, Address):
    pass
```

#### Interaction with `override_doctype_class`

The `extend_doctype_class` hook works on top of `[override_doctype_class](#override-doctype-class)`. If both hooks are defined, extensions are applied to the overridden class instead of the base class.

For example, if ERPNext overrides the Address class and custom apps extend it:

```python
# ERPNext overrides Address
override_doctype_class = {
    "Address": "erpnext.setup.doctype.address.address.Address"
}

# Custom apps extend it
extend_doctype_class = {
    "Address": ["app1.extensions.Prop1Mixin", "app2.extensions.Prop2Mixin"]
}

# Final class becomes:
class ExtendedAddress(Prop2Mixin, Prop1Mixin, ERPNextAddress):
    pass
```

This hook is ideal for adding virtual fields, computed properties, and custom methods without disrupting the core functionality. It's recommended to use this hook instead of `[override_doctype_class](#override-doctype-class)` when you only need to add functionality rather than replace it.

### Override DocType Class

You can override/extend the class for standard doctypes by using the `override_doctype_class` hook. Unlike `[extend_doctype_class](#extend-doctype-class)`, this hook completely replaces the original class. This can cause issues when you have multiple apps overriding the same DocType class. In v16+, prefer using `[extend_doctype_class](#extend-doctype-class)` instead.

**app/hooks.py**

```
override_doctype_class = {
    "ToDo": "app.overrides.todo.CustomToDo"
}
  

```

**app/overrides/todo.py**

```
from frappe.desk.doctype.todo.todo import ToDo

class CustomToDo(ToDo):
    def on_update(self):
        self.my_custom_code()
        super().on_update()

    def my_custom_code(self):
        pass
  

```

> It is recommended that you extend the standard class of the doctype, otherwise you will have to implement all of the core functionality.

### Override Form Scripts

You can override/extend [Standard Form Scripts](/framework/v14/user/en/api/form#standard-form-scripts) by using the `doctype_js` hook.

**app/hooks.py**

```
doctype_js = {
    "ToDo": "public/js/todo.js",
}
  

```

**app/public/js/todo.js**

```
frappe.ui.form.on("Todo", {
    refresh: function(frm) {
        frm.trigger("my_custom_code");
    },
    my_custom_code: function(frm){
        console.log(frm.doc.name)
    }
});
  

```

> The events/functions defined in `app/public/todo.js` will extend > those in the standard form script of `ToDo` doctype.

### CRUD Events

You can hook into various CRUD events of any doctype using the `doc_events` hook.

**app/hooks.py**

```
doc_events = {
    "*": {
        # will run after any DocType record is inserted into database
        "after_insert": "app.crud_events.after_insert_all"
    },
    "ToDo": {
        # will run before a ToDo record is inserted into database
        "before_insert": "app.crud_events.before_insert_todo",
    }
}
  

```

The method will be passed the doc and the method name as arguments.

**app/crud_events.py**

```
def after_insert_all(doc, method=None):
    pass

def before_insert_todo(doc, method=None):
    pass
  

```

> See [Controller Hooks](/framework/v14/user/en/basics/doctypes/controllers#controller-hooks) > for a list of all available hooks.

### Override Whitelisted Methods

Whitelisted Methods are python methods that are accessible on a REST endpoint and consumed by a client. You can override standard whitelisted methods that are part of the core framework using the `override_whitelisted_methods` hook.

**app/hooks.py**

```
override_whitelisted_methods = {
    "frappe.client.get_count": "app.whitelisted.custom_get_count"
}
  

```

The method should have the same signature as the original method.

**app/whitelisted.py**

```
def custom_get_count(doctype, filters=None, debug=False, cache=False):
    # your custom implementation of the standard get_count method provided by frappe
    pass
  

```

### Ignore Links on Delete

To ignore links to specific DocTypes when deleting documents, you can specify them in the `ignore_links_on_delete` hook like so:

**app/hooks.py**

```
ignore_links_on_delete = ["Communication", "ToDo"]
  

```

### Form Timeline

The timeline section of [form view](/framework/v14/user/en/desk#form-view) of a document shows an audit trail of actions performed on that document like views, value changes, comments and related communications, etc.

Apart from these standard actions, there might arise a situation where you need to add your own custom actions. You can do this via `additional_timeline_content` hook.

```
additional_timeline_content: {
    # show in each document's timeline
    "*": ["app.timeline.all_timeline"]
    # only show in ToDo's timeline
    "ToDo": ["app.timeline.todo_timeline"]
}
  

```

The method will be passed the doctype and docname as arguments. You can perform queries and return actions related to that document as a list of dicts as shown in the example. Each dict in the list must have a `creation` value which will be used to sort the item in the timeline.

```
def todo_timeline(doctype, docname):
    # this method should return a list of dicts
    return [
        {
             # this will be used to sort the content in the timeline
            "creation": "22-05-2020 18:00:00",
            # this JS template will be rendered in the timeline
            "template": "custom_timeline_template",
            # this data will be passed to the template.
            "template_data": {"key": "value"},
        },
        ...
    ]
  

```

## Scheduler Events

You can use Scheduler Events for running tasks periodically in the background using the `scheduler_events` hook.

**app/hooks.py**

```
scheduler_events = {
    "hourly": [
        # will run hourly
        "app.scheduled_tasks.update_database_usage"
    ],
}
  

```

**app/scheduled_tasks.py**

```
def update_database_usage():
    pass
  

```

> After changing any scheduled events in `hooks.py`, you need to run `bench migrate` for changes to take effect.

### Available Events

- `hourly`, `daily`, `weekly`, `monthly`

These events will trigger every hour, day, week and month respectively.

- `hourly_long`, `daily_long`, `weekly_long`, `monthly_long`

Same as above but these jobs are run in the [long worker](/framework/v14/user/en/basics/directory-structure#worker_long) suitable for long running jobs.

- `all`

The `all` event is triggered every 60 seconds. This can be configured via the `scheduler_tick_interval` key in `common_site_config.json`

- `cron`

A valid cron string that can be parsed by [croniter](https://pypi.org/project/croniter/).

Usage Examples:

```
scheduler_events = {
    "daily": [
        "app.scheduled_tasks.manage_recurring_invoices"
    ],
    "daily_long": [
        "app.scheduled_tasks..take_backups_daily"
    ],
    "cron": {
        "15 18 * * *": [
            "app.scheduled_tasks..delete_all_barcodes_for_users"
        ],
        "*/6 * * * *": [
            "app.scheduled_tasks..collect_error_snapshots"
        ],
        "annual": [
            "app.scheduled_tasks.collect_error_snapshots"
        ]
    }
}
  

```

## Jinja Customization

Frappe provides a list of [global utility methods](/framework/v14/user/en/api/jinja) in Jinja templates. To add your own methods and filters you can use the `jinja` hook.

**app/hooks.py**

```
jinja = {
    "methods": [
        "app.jinja.methods",
        "app.utils.get_fullname"
    ],
    "filters": [
        "app.jinja.filters",
        "app.utils.format_currency"
    ]
}
  

```

**app/jinja/methods.py**

```
def sum(a, b):
    return a + b

def multiply(a, b):
    return a * b
  

```

> If the path is a module path, all the methods in that module will be added.

**app/utils.py**

```
def get_fullname(user):
    first_name, last_name = frappe.db.get_value("User", user, ["first_name", "last_name"])
    return first_name + " " + last_name

def format_currency(value, currency):
    return currency + " " + str(value)
  

```

Now, you can use these utilities in your Jinja templates like so:

```
<h1>Hi, {{ get_fullname(frappe.session.user) }}</h1>
<p>Your account balance is {{ account_balance | format_currency("INR") }}</p>
<p>1 + 2 = {{ sum(1, 2) }}</p>
  

```

## Prevent Auto Cancellation of Linked Documents

To prevent documents of a specific DocType from being automatically cancelled on the cancellation of any linked documents you can use the `auto_cancel_exempted_doctypes` hook.

**app/hooks.py**

```
auto_cancel_exempted_doctypes = ["Payment Entry"]
  

```

In the above example, if any document (for e.g Sales Invoice) that is linked with Payment Entry is cancelled, it will skip the auto-cancellation of the linked Payment Entry document.

## Notification configurations

The notification configuration hook is used to customize the items shown in the Notification dropdown in Desk. It can be configured by the `notification_config` hook.

**app/hooks.py**

```
notification_config = "app.notification.get_config"
  

```

The method is called without any arguments.

**app/notification.py**

```
def get_config():
 return {
        "for_doctype": {
            "Issue": {"status":"Open"},
            "Issue": {"status":"Open"},
        },
        "for_module_doctypes": {
            "ToDo": "To Do",
            "Event": "Calendar",
            "Comment": "Messages"
        },
        "for_module": {
            "To Do": "frappe.core.notifications.get_things_todo",
            "Calendar": "frappe.core.notifications.get_todays_events",
            "Messages": "frappe.core.notifications.get_unread_messages"
        }
    }
  

```

The above configuration has three parts:

1. `for_doctype` part of the above configuration marks any "Issue" or "Customer Issue" as unread if its status is Open
2. `for_module_doctypes` maps doctypes to module's unread count.
3. `for_module` maps modules to functions to obtain its unread count. The functions are called without any argument.

## Required Apps

When building apps, you might create apps that build on top of other apps. To make sure dependent apps are installed when someone installs your app, you can use the `required_apps` hook.

**app/hooks.py**

```
required_apps = ["erpnext"]
  

```

The above configuration will make sure `erpnext` is installed when someone installs your app.

## User Data Protection &amp; Privacy

User Data Privacy features like personal data download and personal data deletion come out of the box in Frappe. What constitutes as personal data may be defined by the App publisher in the application's `hooks.py` file as `user_data_fields`.

**app/hooks.py**

```
user_data_fields = [
    {"doctype": "Access Log"},
    {"doctype": "Comment", "strict": True},
    {
        "doctype": "Contact",
        "filter_by": "email_id",
        "rename": True,
    },
    {"doctype": "Contact Email", "filter_by": "email_id"},
    {
        "doctype": "File",
        "filter_by": "attached_to_name",
        "redact_fields": ["file_name", "file_url"],
    },
    {"doctype": "Email Unsubscribe", "filter_by": "email", "partial": True},
]
  

```

DocTypes that have user data should be mapped under this hook using the above format. Upon data deletion or download requests from users, this hook will be utilized to map over the specified DocTypes. The options available to modify documents are:




| Field           | Description                                                                                                                                                                                    |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `doctype`       | DocType that contains user data.                                                                                                                                                               |
| `filter_by`     | Docfield to filter the documents by. If unset, defaults to `owner`.                                                                                                                            |
| `partial`       | If set, all text fields are parsed and user's full name and username references will be redacted.                                                                                              |
| `redact_fields` | Fields that have to be redacted. If unspecified, it considers partial data redaction from all text fields.                                                                                     |
| `rename`        | If document name contains user data, set this field to rename document to anonymize it.                                                                                                        |
| `strict`        | If set to True, any user data will be redacted from all documents of current DocType. If unset, it defaults to False which means it only filters through documents in which user is the owner. |


> Note: Personal Data Download only utilizes the doctype and filter_by fields defined in `user_data_fields`

Related Topics:

1. [Personal Data Deletion](https://docs.erpnext.com/docs/user/manual/en/setting-up/personal-data-deletion)
2. [Personal Data Download](https://docs.erpnext.com/docs/user/manual/en/setting-up/personal-data-download)

## Signup Form Template

If you want to add additional fields to the signup form you can use this hook. Create a template file which contains the custom signup form. Pass this template to the custom signup hook.

```
signup_form_template = "school/templates/signup-form.html"
  

```

> Note: If you want custom fields in signup form, it will require additional fields in the user doctype. You will have to add these fields using fixtures. Also you will have to write your own submit handler for this signup form and a function on the server side which will signup the user. This way you can also write validations for the custom fields you add.

## SMS Hooks

These hooks allow you to customize the SMS sending functionality in Frappe by integrating with third-party SMS providers or implementing custom SMS logic.

### Send SMS

This hook allows you to override the default SMS sending logic for notifications, bulk SMS, and other general SMS functionality.

**app/hooks.py**

```python
send_sms = "app.overrides.sms.send_sms"
```

The method is called with the SMS details including recipient, message content, and other metadata.

**app/overrides/sms.py**

```python
def send_sms(receiver_list, msg, sender=None, success_msg=True):
    """
    Override default SMS sending logic
    
    Args:
        receiver_list: List of mobile numbers or single mobile number
        msg: SMS message content
        sender: Sender ID (optional)
        success_msg: Whether to show success message (optional)
    """
   
    # Send SMS via custom provider
    response = custom_sms_provider.send_sms(
        to=receiver_list,
        message=msg,
        from_number=sender
    )
    
    if response.status_code == 200:
        frappe.msgprint(_("SMS sent successfully"))
        return True
    else:
        frappe.throw(_("Failed to send SMS"))
        return False
```

### Send Token via SMS

This hook allows you to override SMS OTP sending logic for two-factor authentication and mobile login flows.

**app/hooks.py**

```python
send_token_via_sms = "app.overrides.sms.send_token_via_sms"
```

The method is called with OTP details for authentication purposes.

**app/overrides/sms.py**

```python
def send_token_via_sms(otpsecret, token=None, phone_no=None):
   """
    Generate OTP and send using local send_otp function.
    :param otpsecret: OTP secret for generating HOTP
    :param token: Token to use for HOTP generation
    :param phone_no: Phone number to send OTP to
    """
    if not phone_no:
        return False
    try:
        hotp = pyotp.HOTP(otpsecret)
        otp_code = hotp.at(token_int)
        result = send_otp(
            number=phone_no,
            otp_length=len(otp_code),
            otp_expiry=5,  # 5 minutes expiry
            otp=otp_code
        )
        return result.get("success", False)
    except Exception as e:
        frappe.log_error(
            message=f"Failed to send OTP: {str(e)}",
            title="OTP Error"
        )
        return False 
```

## List of available hooks


| Hook Name                                | Explanation                                                                                                               |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `additional_timeline_content`            | [Form Timeline](#form-timeline)                                                                                           |
| `after_install`                          | [Install Hooks](#install-hooks)                                                                                           |
| `after_migrate`                          | [Migrate Hooks](#migrate-hooks)                                                                                           |
| `after_sync`                             | [Install Hooks](#install-hooks)                                                                                           |
| `app_include_css`                        | [Desk Assets](#desk)                                                                                                      |
| `app_include_js`                         | [Desk Assets](#desk)                                                                                                      |
| `app_logo_url`                           | [App Meta Data](#app-meta-data)                                                                                           |
| `app_title`                              | [App Meta Data](#app-meta-data)                                                                                           |
| `auto_cancel_exempted_doctypes`          | [Prevent Auto Cancellation](#prevent-auto-cancellation-of-linked-documents)                                               |
| `base_template_map`                      | [Base Template](#base-template)                                                                                           |
| `base_template`                          | [Base Template](#base-template)                                                                                           |
| `before_install`                         | [Install Hooks](#install-hooks)                                                                                           |
| `before_migrate`                         | [Migrate Hooks](#migrate-hooks)                                                                                           |
| `before_tests`                           | [Test Hooks](#test-hooks)                                                                                                 |
| `before_write_file`                      | [File Hooks](#file-hooks)                                                                                                 |
| `bot_parsers`                            | *Deprecated*                                                                                                              |
| `braintree_success_page`                 | [Braintree Success Page](#braintree-success-page)                                                                         |
| `brand_html`                             | [Brand HTML](#brand-html)                                                                                                 |
| `calendars`                              | [Calendars](#calendars)                                                                                                   |
| `clear_cache`                            | [Clear Cache](#clear-cache)                                                                                               |
| `communication_doctypes`                 |                                                                                                                           |
| `default_mail_footer`                    | [Default Mail Footer](#default-mail-footer)                                                                               |
| `delete_file_data_content`               | [File Hooks](#file-hooks)                                                                                                 |
| `doc_events`                             | [Document CRUD Events](#crud-events)                                                                                      |
| `doctype_js`                             | [Override Form Scripts](#override-form-scripts)                                                                           |
| `domains`                                |                                                                                                                           |
| `dump_report_map`                        | *Deprecated*                                                                                                              |
| `extend_bootinfo`                        | [Extend Bootinfo](#extend-bootinfo)                                                                                       |
| `extend_website_page_controller_context` | [Website Controller Context](#website-controller-context)                                                                 |
| `filters_config`                         |                                                                                                                           |
| `fixtures`                               | [Fixtures](#fixtures)                                                                                                     |
| `get_site_info`                          |                                                                                                                           |
| `get_translated_dict`                    |                                                                                                                           |
| `get_website_user_home_page`             | [Default Homepage](#default-homepage)                                                                                     |
| `get_web_pages_with_dynamic_routes`      | [Web pages with dynamic routes](#web-pages-with-dynamic-routes)                                                           |
| `has_permission`                         | [Document Permissions](#document-permissions)                                                                             |
| `has_website_permission`                 |                                                                                                                           |
| `home_page`                              | [Default Homepage](#default-homepage)                                                                                     |
| `jenv`                                   | [Jinja Customization](#jinja-customization)                                                                               |
| `leaderboards`                           |                                                                                                                           |
| `look_for_sidebar_json`                  |                                                                                                                           |
| `make_email_body_message`                |                                                                                                                           |
| `notification_config`                    | [Notification configuration](#notification-configurations)                                                                |
| `on_login`                               | [Session Hooks](#session-hooks)                                                                                           |
| `on_logout`                              | [Session Hooks](#session-hooks)                                                                                           |
| `on_print_pdf`                           | On printing                                                                                                               |
| `on_session_creation`                    | [Session Hooks](#session-hooks)                                                                                           |
| `extend_doctype_class`                   | [Extend DocType Class](#extend-doctype-class)                                                                             |
| `override_doctype_class`                 | [Override DocType Class](#override-doctype-class)                                                                         |
| `override_doctype_dashboards`            |                                                                                                                           |
| `override_whitelisted_methods`           | [Override Whitelisted Methods](#override-whitelisted-methods)                                                             |
| `ignore_links_on_delete`                 | [Ignore Links on Delete](#ignore-links-on-delete)                                                                         |
| `permission_query_conditions`            | [Modify List Query](#modify-list-query)                                                                                   |
| `portal_menu_items`                      | [Portal Sidebar](#portal-sidebar)                                                                                         |
| `required_apps`                          | [Required Apps](#required-apps)                                                                                           |
| `role_home_page`                         | [Default Homepage](#default-homepage)                                                                                     |
| `scheduler_events`                       | [Scheduler Events](#scheduler-events)                                                                                     |
| `setup_wizard_complete`                  |                                                                                                                           |
| `setup_wizard_exception`                 |                                                                                                                           |
| `setup_wizard_requires`                  |                                                                                                                           |
| `setup_wizard_stages`                    |                                                                                                                           |
| `setup_wizard_success`                   |                                                                                                                           |
| `signup_form_template`                   | [Signup Form Template](#signup-form-template)                                                                             |
| `sounds`                                 | [Sounds](#sounds)                                                                                                         |
| `standard_portal_menu_items`             | [Portal Sidebar](#portal-sidebar)                                                                                         |
| `standard_queries`                       | [Standard Queries](https://www.linkedin.com/pulse/understanding-standardqueries-hook-frappe-framework-el-shafeai--m4ozf/) |
| `send_sms`                               | [SMS Hooks](#send-sms)                                                                                                    |
| `send_token_via_sms`                     | [SMS Hooks](#send-token-via-sms)                                                                                          |
| `template_apps`                          |                                                                                                                           |
| `translated_languages_for_website`       |                                                                                                                           |
| `translator_url`                         |                                                                                                                           |
| `treeviews`                              | DocTypes that use TreeView as the default view (instead of ListView)                                                      |
| `update_website_context`                 | [Website Context](#website-context)                                                                                       |
| `user_privacy_documents`                 | *Deprecated* (Use `user_data_fields` hook)                                                                                |
| `user_data_fields`                       | [User Data Protection &amp; Privacy](#user-data-protection-&amp;-privacy)                                                 |
| `web_include_css`                        | [Portal Assets](#portal)                                                                                                  |
| `web_include_js`                         | [Portal Assets](#portal)                                                                                                  |
| `website_catch_all`                      | [Website 404](#website-404)                                                                                               |
| `website_clear_cache`                    | [Website Clear Cache](#website-clear-cache)                                                                               |
| `website_context`                        | [Website Context](#website-context)                                                                                       |
| `website_generators`                     | *Deprecated* (Use Has Web View in DocType instead)                                                                        |
| `website_redirects`                      | [Website Redirects](#website-redirects)                                                                                   |
| `website_route_rules`                    | [Website Route Rules](#website-route-rules)                                                                               |
| `website_user_home_page`                 | *Deprecated* (Use `homepage` hook)                                                                                        |
| `welcome_email`                          |                                                                                                                           |
| `write_file_keys`                        | *Deprecated*                                                                                                              |
| `write_file`                             | [File Hooks](#file-hooks)                                                                                                 |
