# How to Implement Custom Document Validation in ERPNext Hooks: A Complete Guide

> Learn to implement custom document validation in ERPNext using doc_events hooks. Inject custom logic into your Python functions to ensure data integrity without altering core files.

- Repository: [Frappe/erpnext](https://github.com/frappe/erpnext)
- Tags: how-to-guide
- Published: 2026-05-20

---

**You implement custom document validation in ERPNext by defining a `doc_events` hook in your custom app's [`hooks.py`](https://github.com/frappe/erpnext/blob/main/hooks.py) file, mapping DocTypes and events (like `validate`) to Python functions that receive the `doc` object and `method` name, allowing you to inject validation logic without modifying core ERPNext files.**

ERPNext's hook architecture provides a clean extension point for enforcing business rules across any DocType. By leveraging the **`doc_events`** dictionary defined in the core [`erpnext/hooks.py`](https://github.com/frappe/erpnext/blob/main/erpnext/hooks.py), you can attach custom Python functions to document lifecycle events without touching the framework's source code. This guide demonstrates how to implement custom document validation in ERPNext hooks using the exact patterns found in the frappe/erpnext repository.

## How the doc_events Hook Works

The entry point for document-level hooks resides in [`erpnext/hooks.py`](https://github.com/frappe/erpnext/blob/main/erpnext/hooks.py) at lines 51-57, where ERPNext defines a global **`doc_events`** dictionary that maps DocTypes to event handlers. During startup, the framework reads this configuration via `frappe.get_hooks("doc_events")` and merges it with hooks from all installed custom apps.

```python

# erpnext/hooks.py – doc_events definition (source: lines 51-57)

doc_events = {
    "*": {
        "validate": [
            "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
            "erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.check_for_running_deletion_job",
        ],
    },
}

```

When a document triggers an event like `validate` or `before_save`, ERPNext iterates over the corresponding list and calls each dotted-path method, passing the current **`doc`** object as the first argument and the event **`method`** name as the second. You can target specific DocTypes by name or use the `"*"` wildcard to apply logic globally.

## Step-by-Step Implementation

### Create the Validation Function

Place your validation logic in any Python module importable by your custom app. The function must accept exactly two parameters: `doc` (the document instance) and `method` (the event string).

```python

# my_custom_app/validation.py

import frappe
from frappe import _

def validate_sales_order(doc, method):
    """Abort save if total is negative."""
    if doc.total < 0:
        frappe.throw(_("Total amount cannot be negative"))

```

### Register the Hook in hooks.py

Expose the function by adding an entry to your app's [`hooks.py`](https://github.com/frappe/erpnext/blob/main/hooks.py). The key is the DocType name, the sub-key is the event, and the value is the dotted Python path to your function.

```python

# my_custom_app/hooks.py

doc_events = {
    "Sales Order": {
        "validate": "my_custom_app.validation.validate_sales_order"
    }
}

```

### Deploy Your Custom App

Install the app into your site or add it to [`sites/apps.txt`](https://github.com/frappe/erpnext/blob/main/sites/apps.txt). Once deployed, ERPNext automatically merges your `doc_events` with the core dictionary at startup. Every subsequent save operation on the target DocType executes your validation function.

## Practical Validation Examples

### Mandatory Field Validation by Item Group

Enforce business rules conditionally based on field values. This example requires an SKU for all items categorized under "Electronics".

```python

# my_custom_app/validation.py

def validate_custom_item(doc, method):
    if doc.item_group == "Electronics" and not doc.sku:
        frappe.throw(_("SKU is required for electronic items"))

```

```python

# my_custom_app/hooks.py

doc_events = {
    "Item": {
        "validate": "my_custom_app.validation.validate_custom_item"
    }
}

```

### Cross-Document Credit Limit Checks

Validation hooks can query related documents. This example blocks Sales Invoice submission if the grand total exceeds the customer's configured credit limit.

```python

# my_custom_app/validation.py

def validate_sales_invoice_credit(doc, method):
    customer = frappe.get_doc("Customer", doc.customer)
    if doc.grand_total > customer.credit_limit:
        frappe.throw(
            _("Invoice total exceeds customer's credit limit of {0}").format(customer.credit_limit)
        )

```

```python

# my_custom_app/hooks.py

doc_events = {
    "Sales Invoice": {
        "validate": "my_custom_app.validation.validate_sales_invoice_credit"
    }
}

```

### Preventing Critical Document Deletion

Use the `before_delete` event to block removal of sensitive records. This triggers before the framework removes the document from the database.

```python

# my_custom_app/validation.py

def prevent_deletion_of_critical_doc(doc, method):
    if doc.is_critical:
        frappe.throw(_("Critical documents cannot be deleted"))

```

```python

# my_custom_app/hooks.py

doc_events = {
    "My Critical Doctype": {
        "before_delete": "my_custom_app.validation.prevent_deletion_of_critical_doc"
    }
}

```

## Technical Architecture Behind the Hooks

ERPNext reads the `doc_events` dictionary at boot time using `frappe.get_hooks("doc_events")`, which aggregates entries from all installed apps. When a document event fires, the framework invokes `frappe.call()` on each registered method, supplying the current document instance. Because this architecture relies on dotted-path strings rather than direct imports, your custom app remains decoupled from core ERPNext code, ensuring seamless upgrades and clean separation of concerns.

## Summary

- **Define** validation logic in Python functions accepting `doc` and `method` parameters.
- **Register** functions in your app's [`hooks.py`](https://github.com/frappe/erpnext/blob/main/hooks.py) under the `doc_events` dictionary, mapping DocTypes and events (e.g., `validate`, `before_delete`).
- **Abort** transactions by calling **`frappe.throw()`** inside your validation function.
- **Target** specific DocTypes by name or all DocTypes using the `"*"` wildcard.
- **Maintain** core code integrity by keeping custom logic in separate apps that merge at runtime.

## Frequently Asked Questions

### What parameters must a document validation function accept?

Your function must accept at least two arguments: **`doc`** (the current document object) and **`method`** (the string name of the event, such as `"validate"`). You may include additional keyword arguments, but these two are required for the framework to execute the hook correctly.

### How do I target all DocTypes with a single validation hook?

Use the **`"*"`** wildcard as the DocType key in your `doc_events` dictionary. This applies the validation to every document in the system. ERPNext uses this pattern internally at line 52 of [`erpnext/hooks.py`](https://github.com/frappe/erpnext/blob/main/erpnext/hooks.py) to run global checks like service level agreement validation.

### Can I prevent a document from being saved using a validation hook?

Yes. Raise a **`frappe.throw()`** exception with a descriptive message inside your validation function. This aborts the database transaction and displays the error to the user, preventing the save operation from completing.

### Where does ERPNext read the doc_events configuration from?

The framework reads `doc_events` from the **[`hooks.py`](https://github.com/frappe/erpnext/blob/main/hooks.py)** file of every installed app at server startup. It specifically looks for the `doc_events` dictionary and merges all entries, as seen in the source code at [`erpnext/hooks.py`](https://github.com/frappe/erpnext/blob/main/erpnext/hooks.py) lines 51-57, creating a unified mapping of DocTypes to their event handlers.